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