Spaces:
Sleeping
Sleeping
Create MMSE.py
Browse files
MMSE.py
ADDED
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# MMSE.py
|
2 |
+
# Contains all data, scoring logic, and setup specific to the
|
3 |
+
# Mini-Mental State Examination (MMSE).
|
4 |
+
|
5 |
+
import os
|
6 |
+
import time
|
7 |
+
from gtts import gTTS
|
8 |
+
|
9 |
+
# Import shared utilities
|
10 |
+
import utils
|
11 |
+
|
12 |
+
# --- MMSE Specific Data ---
|
13 |
+
# Use the 'now' object and 'get_season' function from the utils module
|
14 |
+
now = utils.now
|
15 |
+
GROUPED_QUESTIONS = {
|
16 |
+
"Question 1: Temporal Orientation": {
|
17 |
+
"What year is this?": {"answer": str(now.year), "instruction": "Score 1 point for the correct year."},
|
18 |
+
"What season is this in Northern Hemisphere?": {"answer": utils.get_season(now.month), "instruction": "Examples: Summer, Fall, Winter, Spring"},
|
19 |
+
"What month is this?": {"answer": now.strftime("%B").lower(), "instruction": "Examples: january, february, ..."},
|
20 |
+
"What is the day of today's date?": {"answer": str(now.day), "instruction": "Examples: Range from 1 to 31"},
|
21 |
+
"What day of the week is this?": {"answer": now.strftime("%A").lower(), "instruction": "Examples: monday, tuesday, ..."}
|
22 |
+
},
|
23 |
+
"Question 2: Spatial Orientation": {
|
24 |
+
"What country are we in?": {"answer": "united states"}, "What state are we in?": {"answer": "connecticut"},
|
25 |
+
"What city or town are we in?": {"answer": "greenwich"}, "What is the street address / name of building?": {"answer": "123 main street"},
|
26 |
+
"What room or floor are we in?": {"answer": "living room"},
|
27 |
+
},
|
28 |
+
"Question 3: Memory Registration": {
|
29 |
+
": I am going to name three words. Repeat these three words: Ball Car Man": {
|
30 |
+
"answer": "ball car man",
|
31 |
+
"instruction": "Say the words clearly at one per second. After response, say 'Keep those words in mind. I will ask for them again.'",
|
32 |
+
"max_points": 3
|
33 |
+
}
|
34 |
+
},
|
35 |
+
"Question 4: Attention": {
|
36 |
+
"Count backward from 100 substracting by sevens": {
|
37 |
+
"answer": "93 86 79 72 65",
|
38 |
+
"instruction": "Stop after five subtractions. Score one point for each correct number.",
|
39 |
+
"max_points": 5
|
40 |
+
}
|
41 |
+
},
|
42 |
+
"Question 5: Delayed Recall": {"What were the three words I asked you to remember?": {"answer": "ball car man", "max_points": 3}},
|
43 |
+
"Question 6: Naming Communication": {
|
44 |
+
"I am going to show you the first object and I would like you to name it": {"answer": "watch|wristwatch", "instruction": "Show the patient a watch.", "max_points": 1},
|
45 |
+
"I am going to show you the second object and I would like you to name it": {"answer": "pencil", "instruction": "Show the patient a pencil.", "max_points": 1}
|
46 |
+
},
|
47 |
+
"Question 7: Sentence Repetition": {"I would like you to repeat a phrase after me: No ifs, ands, or buts.": {"answer": "no ifs, ands, or buts", "max_points": 1}},
|
48 |
+
"Question 8: Praxis 3-Stage Movement": {
|
49 |
+
"Take this paper in your non-dominant hand, fold the paper in half once with both hands and put the paper down on the floor.": {
|
50 |
+
"answer": "A numeric value from 0 to 3 representing tasks completed.",
|
51 |
+
"instruction": "Input how many tasks were completed (0 to 3).", "max_points": 3
|
52 |
+
}
|
53 |
+
},
|
54 |
+
"Question 9: Reading on CLOSE YOUR EYES": {"Read the CAPITALIZED words on this question and then do what it says": {"answer": "yes", "instruction": "Input 'yes' if eyes are closed; else, 'no'.", "max_points": 1}},
|
55 |
+
"Question 10: Writing Communication": {"Write any complete sentence here or on a piece of paper": {"answer": "A sentence containing at least one noun and one verb.", "max_points": 1}},
|
56 |
+
"Question 11: Visuoconstruction": {
|
57 |
+
"Please draw a copy of this picture": {
|
58 |
+
"answer": "4", "instruction": "Show them a drawing of two overlapping pentagons. Ask them to draw a copy.", "max_points": 1
|
59 |
+
}
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
# --- Derived Data Structures (for the UI to use) ---
|
64 |
+
STRUCTURED_QUESTIONS = []
|
65 |
+
main_num = 1
|
66 |
+
for section, questions in GROUPED_QUESTIONS.items():
|
67 |
+
main_cat_name = section.split(":", 1)[1].strip() if ":" in section else section
|
68 |
+
sub_q_idx = 0
|
69 |
+
for question, data in questions.items():
|
70 |
+
STRUCTURED_QUESTIONS.append({
|
71 |
+
"main_cat": main_cat_name, "main_num": main_num, "sub_letter": chr(ord('a') + sub_q_idx),
|
72 |
+
"question": question, "answer": data["answer"], "instruction": data.get("instruction", ""),
|
73 |
+
"max_points": data.get("max_points", 1)
|
74 |
+
})
|
75 |
+
sub_q_idx += 1
|
76 |
+
main_num += 1
|
77 |
+
|
78 |
+
TOTAL_QUESTIONS = len(STRUCTURED_QUESTIONS)
|
79 |
+
QUESTION_CHOICES = [f"Q{q['main_num']}{q['sub_letter']}: {q['question']}" for q in STRUCTURED_QUESTIONS]
|
80 |
+
DRAWING_Q_INDEX = next((i for i, q in enumerate(STRUCTURED_QUESTIONS) if "draw a copy" in q["question"]), -1)
|
81 |
+
|
82 |
+
|
83 |
+
# --- MMSE Specific Audio Handling ---
|
84 |
+
AUDIO_FILE_MAP = {}
|
85 |
+
def pregenerate_audio():
|
86 |
+
"""Pre-generates all TTS audio at startup to avoid rate-limiting."""
|
87 |
+
print("Pre-generating MMSE TTS audio...")
|
88 |
+
for i, q_data in enumerate(STRUCTURED_QUESTIONS):
|
89 |
+
try:
|
90 |
+
tts = gTTS(q_data['question'].strip())
|
91 |
+
filepath = f"/tmp/question_{i}.mp3"
|
92 |
+
tts.save(filepath)
|
93 |
+
AUDIO_FILE_MAP[i] = filepath
|
94 |
+
time.sleep(0.5) # Pause to avoid rate-limiting
|
95 |
+
except Exception as e:
|
96 |
+
print(f"Warning: Could not pre-generate audio for question {i}: {e}")
|
97 |
+
AUDIO_FILE_MAP[i] = None
|
98 |
+
print("MMSE TTS audio pre-generation complete.")
|
99 |
+
|
100 |
+
def speak_question(current_index):
|
101 |
+
"""Returns the file path for the pre-generated audio of the current question."""
|
102 |
+
return AUDIO_FILE_MAP.get(current_index)
|
103 |
+
|
104 |
+
|
105 |
+
# --- MMSE Specific Scoring Functions ---
|
106 |
+
def score_sevens_response(cleaned_user_input):
|
107 |
+
"""Scores the sevens question from cleaned, space-separated numbers."""
|
108 |
+
correct_numbers = {"93", "86", "79", "72", "65"}
|
109 |
+
user_numbers = set((cleaned_user_input or "").split())
|
110 |
+
return len(correct_numbers.intersection(user_numbers))
|
111 |
+
|
112 |
+
def score_three_words_response(cleaned_user_input):
|
113 |
+
"""Scores the three words question from cleaned text."""
|
114 |
+
correct_words = {"ball", "car", "man"}
|
115 |
+
user_words = set((cleaned_user_input or "").split())
|
116 |
+
return len(correct_words.intersection(user_words))
|
117 |
+
|
118 |
+
|
119 |
+
# --- Main Evaluation Logic ---
|
120 |
+
def evaluate(answers_list, user_drawing_path):
|
121 |
+
"""
|
122 |
+
Evaluates all MMSE answers and returns the results.
|
123 |
+
This function is now UI-agnostic. It returns data, not UI components.
|
124 |
+
"""
|
125 |
+
total_score, total_possible_score, results = 0, 0, []
|
126 |
+
|
127 |
+
for i, q_data in enumerate(STRUCTURED_QUESTIONS):
|
128 |
+
user_answer = answers_list[i]
|
129 |
+
point = 0
|
130 |
+
normalized_answer = utils.normalize_numeric_words(user_answer)
|
131 |
+
|
132 |
+
# Routing logic for different scoring types
|
133 |
+
if i == DRAWING_Q_INDEX:
|
134 |
+
try:
|
135 |
+
expected_sides = int(q_data["answer"])
|
136 |
+
except (ValueError, TypeError):
|
137 |
+
expected_sides = 0
|
138 |
+
point, sides_detected = utils.score_drawing(user_drawing_path, expected_sides)
|
139 |
+
if sides_detected > 0:
|
140 |
+
user_answer = f"[{sides_detected}-sided shape detected]"
|
141 |
+
elif user_drawing_path and os.path.exists(user_drawing_path):
|
142 |
+
user_answer = "[Image uploaded, but no clear shape found]"
|
143 |
+
else:
|
144 |
+
user_answer = "[No image uploaded]"
|
145 |
+
|
146 |
+
elif "Write any complete sentence" in q_data["question"]:
|
147 |
+
point = utils.score_sentence_structure(user_answer)
|
148 |
+
elif "substracting by sevens" in q_data["question"]:
|
149 |
+
point = score_sevens_response(utils.clean_text_answer(normalized_answer))
|
150 |
+
elif "three words" in q_data["question"]:
|
151 |
+
point = score_three_words_response(utils.clean_text_answer(user_answer))
|
152 |
+
elif "day of today's date" in q_data["question"]:
|
153 |
+
normalized_day = utils.normalize_date_answer(user_answer)
|
154 |
+
point = 1 if normalized_day == str(now.day) else 0
|
155 |
+
elif "Take this paper" in q_data["question"]:
|
156 |
+
point = 0
|
157 |
+
try:
|
158 |
+
numeric_score = int(utils.clean_numeric_answer(normalized_answer))
|
159 |
+
point = min(numeric_score, q_data["max_points"])
|
160 |
+
except (ValueError, TypeError):
|
161 |
+
point = 0
|
162 |
+
else:
|
163 |
+
point = utils.score_keyword_match(q_data["answer"], utils.clean_text_answer(normalized_answer))
|
164 |
+
if point == 1:
|
165 |
+
point = q_data["max_points"]
|
166 |
+
|
167 |
+
result_string = (f"Q{q_data['main_num']}{q_data['sub_letter']}: {q_data['question']}\n"
|
168 |
+
f" - Score: {point} / {q_data['max_points']} | Your Answer: '{user_answer}' | Expected: '{q_data['answer']}'")
|
169 |
+
results.append(result_string)
|
170 |
+
total_score += point
|
171 |
+
total_possible_score += q_data["max_points"]
|
172 |
+
|
173 |
+
# Return pure data
|
174 |
+
return "\n\n".join(results), f"{total_score} / {total_possible_score}"
|