MinaNasser commited on
Commit
087485b
Β·
1 Parent(s): 4f96034

12_voiceAPI

Browse files
Files changed (3) hide show
  1. .env +13 -13
  2. main.py +3 -2
  3. routes/voice_route.py +145 -0
.env CHANGED
@@ -53,22 +53,22 @@ COHERE_API_KEY="getAone"
53
 
54
 
55
  # ---------- MISTRAL ----------
56
- MISTRAL_API_KEY="getAone"
57
- # GENERATION_BACKEND="MISTRAL"
58
- # EMBEDDING_BACKEND="MISTRAL"
59
- # GENERATION_MODEL_ID="mistral-small-2603"
60
- # EMBEDDING_MODEL_ID="mistral-embed-2312"
61
- # EMBEDDING_MODEL_SIZE=1024
62
- # QDRANT_COLLECTION="1024_docs"
63
 
64
  # ---------- GEMINI ----------
65
  GEMINI_API_KEY="getAone"
66
- GENERATION_BACKEND="GEMINI"
67
- EMBEDDING_BACKEND="GEMINI"
68
- GENERATION_MODEL_ID="gemini-2.5-flash"
69
- EMBEDDING_MODEL_ID="gemini-embedding-001"
70
- EMBEDDING_MODEL_SIZE=768
71
- QDRANT_COLLECTION="768_docs"
72
 
73
  # ---------- HUGGING FACE ----------
74
  HF_API_KEY="getAone"
 
53
 
54
 
55
  # ---------- MISTRAL ----------
56
+ MISTRAL_API_KEY="BScWRb6OT6xjplE6MJslLWPcLy5BNsjG"
57
+ GENERATION_BACKEND="MISTRAL"
58
+ EMBEDDING_BACKEND="MISTRAL"
59
+ GENERATION_MODEL_ID="mistral-small-2603"
60
+ EMBEDDING_MODEL_ID="mistral-embed-2312"
61
+ EMBEDDING_MODEL_SIZE=1024
62
+ QDRANT_COLLECTION="1024_docs"
63
 
64
  # ---------- GEMINI ----------
65
  GEMINI_API_KEY="getAone"
66
+ # GENERATION_BACKEND="GEMINI"
67
+ # EMBEDDING_BACKEND="GEMINI"
68
+ # GENERATION_MODEL_ID="gemini-2.5-flash"
69
+ # EMBEDDING_MODEL_ID="gemini-embedding-001"
70
+ # EMBEDDING_MODEL_SIZE=768
71
+ # QDRANT_COLLECTION="768_docs"
72
 
73
  # ---------- HUGGING FACE ----------
74
  HF_API_KEY="getAone"
main.py CHANGED
@@ -4,7 +4,7 @@ from routes.base import base_router
4
  from routes.assisstant_rag import assisstant_router
5
  from routes.exam_router import exam_router
6
  from routes.exam_grading_router import grading_router
7
-
8
  app = FastAPI()
9
  app.add_middleware(
10
  CORSMiddleware,
@@ -17,4 +17,5 @@ app.add_middleware(
17
  app.include_router(base_router)
18
  app.include_router(assisstant_router)
19
  app.include_router(exam_router)
20
- app.include_router(grading_router)
 
 
4
  from routes.assisstant_rag import assisstant_router
5
  from routes.exam_router import exam_router
6
  from routes.exam_grading_router import grading_router
7
+ from routes.voice_route import voice_router
8
  app = FastAPI()
9
  app.add_middleware(
10
  CORSMiddleware,
 
17
  app.include_router(base_router)
18
  app.include_router(assisstant_router)
19
  app.include_router(exam_router)
20
+ app.include_router(grading_router)
21
+ app.include_router(voice_router)
routes/voice_route.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel, Field
3
+ from generation.AssistantRagGenerator import AssistantRagGen
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ voice_router = APIRouter(prefix="/voice", tags=["voice"])
9
+ gen = AssistantRagGen()
10
+
11
+
12
+
13
+ class ScoreRequest(BaseModel):
14
+ userid: str = Field(..., description="Unique identifier of the user")
15
+ examsessionid: str = Field(..., description="Exam session identifier")
16
+ transcribedrecording: str = Field(..., min_length=1, description="Raw STT transcript")
17
+
18
+ class ScoreResponse(BaseModel):
19
+ userid: str
20
+ examsessionid: str
21
+ transcribedrecording: str
22
+ cheatingscorefrom10: float
23
+ reasoning: str | None = None
24
+
25
+ SYSTEM_PROMPT = """
26
+ You are a strict academic integrity analyst for oral exams. You evaluate spoken transcripts for cheating likelihood.
27
+
28
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29
+ STEP 1 β€” TRANSCRIPT CORRECTION
30
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31
+ Fix STT (Speech-To-Text) errors only. Rules:
32
+ - Fix misheared words, broken homophones, run-on words
33
+ - Preserve the student's original phrasing and ideas exactly
34
+ - Support Egyptian Arabic (عربي Ω…Ψ΅Ψ±ΩŠ), mixed Arabic-English (Arabizi), and full English
35
+ - Egyptian Arabic examples to correct: "el moshkela" β†’ keep as-is, "bitshΨͺΨΊΩ„" β†’ fix only STT noise
36
+ - Do NOT rephrase, improve, or add ideas
37
+
38
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39
+ STEP 2 β€” CHEATING SCORE (0.0 to 10.0)
40
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41
+ Score cheating likelihood. Be STRICT and CALIBRATED.
42
+
43
+ HIGH SCORE TRIGGERS (each adds 2–4 points):
44
+ - Zero verbal fillers (no "um", "uh", "eh", "ΩŠΨΉΩ†ΩŠ", "Ψ§Ω…Ω…Ω…", "زي", "ΩƒΨ―Ω‡") in a long answer
45
+ - Exact dates, edition numbers, author names, chapter references mid-speech
46
+ - Textbook-perfect definitions with formal academic syntax
47
+ - Abrupt complexity spike β€” starts casual then suddenly goes academic
48
+ - Perfectly balanced comparisons that read like documentation
49
+ - No personal framing ("I think", "Ω…Ω† ΩˆΨ¬Ω‡Ψ© Ω†ΨΈΨ±ΩŠ", "as far as I know")
50
+ - Answer reads identically to how it would appear in a Wikipedia article or textbook
51
+
52
+ MEDIUM SCORE TRIGGERS (each adds 1–2 points):
53
+ - Correct but suspiciously complete answer with no hesitation
54
+ - Consistent formal register with no casual speech patterns
55
+ - Named theorem/law/algorithm with full historical attribution
56
+
57
+ LOW SCORE INDICATORS (reduce score):
58
+ - Natural hesitations ("um", "uh", "ΩŠΨΉΩ†ΩŠ", "Ψ§Ω…Ω…Ω…", "eh")
59
+ - Self-corrections mid-sentence
60
+ - Informal phrasing or personal framing
61
+ - Partial or slightly incorrect information (real recall)
62
+ - Mixed Arabic-English as natural code-switching (not copying)
63
+
64
+ IMPORTANT RULES:
65
+ - A perfectly clean answer with NO verbal fillers in 3+ sentences = minimum 6.0
66
+ - Wikipedia/textbook-identical phrasing = minimum 7.5
67
+ - Exact citations (author, year, chapter) mid-speech = minimum 8.5
68
+ - Do NOT reward clean answers β€” real oral exams always have some imperfection
69
+ - Egyptian students code-switching between Arabic and English is NORMAL, do not penalise
70
+
71
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72
+ OUTPUT FORMAT β€” strict JSON only, no markdown:
73
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
+ {
75
+ "corrected_transcript": "<corrected text, same language as input>",
76
+ "cheating_score": <float 0.0–10.0>,
77
+ "reasoning": "<one sentence citing the specific signals that determined the score>"
78
+ }
79
+ """.strip()
80
+
81
+
82
+
83
+ @voice_router.post("/score", response_model=ScoreResponse)
84
+ async def stt_ai_score(request: ScoreRequest) -> ScoreResponse:
85
+
86
+ user_message = f"Exam transcript to evaluate:\n\n{request.transcribedrecording}"
87
+
88
+ try:
89
+ raw_output = gen.generator.generate_text(f"{SYSTEM_PROMPT}\n\n{user_message}")
90
+ except Exception as e:
91
+ raise HTTPException(status_code=502, detail=f"Generation backend error: {e}")
92
+
93
+ try:
94
+ parsed = extract_json_payload(raw_output)
95
+
96
+ corrected_transcript: str = parsed["corrected_transcript"]
97
+ cheating_score: float = float(parsed["cheating_score"])
98
+ cheating_score = max(0.0, min(10.0, cheating_score))
99
+
100
+ except (KeyError, ValueError) as e:
101
+ raise HTTPException(
102
+ status_code=500,
103
+ detail=f"Failed to parse model response: {e}. Raw: {str(raw_output)[:300]}",
104
+ )
105
+
106
+ return ScoreResponse(
107
+ userid=request.userid,
108
+ examsessionid=request.examsessionid,
109
+ transcribedrecording=corrected_transcript,
110
+ cheatingscorefrom10=cheating_score,
111
+ reasoning=parsed.get("reasoning"),
112
+ )
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+ def extract_json_payload(raw: Any) -> dict:
121
+ if isinstance(raw, dict):
122
+ if "corrected_transcript" in raw:
123
+ return raw
124
+ for val in raw.values():
125
+ try:
126
+ return extract_json_payload(val)
127
+ except ValueError:
128
+ continue
129
+
130
+ if isinstance(raw, str):
131
+ clean = re.sub(r"```(?:json)?|```", "", raw).strip()
132
+ try:
133
+ parsed = json.loads(clean)
134
+ return extract_json_payload(parsed) # recurse on parsed result
135
+ except json.JSONDecodeError:
136
+ pass
137
+
138
+ if isinstance(raw, list):
139
+ for item in raw:
140
+ try:
141
+ return extract_json_payload(item)
142
+ except ValueError:
143
+ continue
144
+
145
+ raise ValueError(f"Could not find target payload in: {str(raw)[:200]}")