|
|
|
|
|
|
|
|
|
import os |
|
import re |
|
import time |
|
from gtts import gTTS |
|
|
|
|
|
import utils |
|
|
|
|
|
|
|
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 (a subject and a verb) 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 |
|
} |
|
} |
|
} |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
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) |
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
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_raw = answers_list[i] |
|
|
|
|
|
|
|
|
|
match = re.search(r'\((.*?)\)', user_answer_raw) |
|
if match: |
|
answer_for_scoring = match.group(1) |
|
else: |
|
answer_for_scoring = user_answer_raw |
|
|
|
|
|
point = 0 |
|
normalized_answer = utils.normalize_numeric_words(answer_for_scoring) |
|
|
|
|
|
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) |
|
display_answer = f"[{sides_detected}-sided shape detected]" if sides_detected > 0 else "[Image provided]" |
|
elif "Write any complete sentence" in q_data["question"]: |
|
|
|
original_part = user_answer_raw.split('(')[0].strip() |
|
point = utils.score_sentence_structure(original_part) |
|
display_answer = user_answer_raw |
|
elif "substracting by sevens" in q_data["question"]: |
|
point = score_sevens_response(utils.clean_text_answer(normalized_answer)) |
|
display_answer = user_answer_raw |
|
elif "three words" in q_data["question"]: |
|
point = score_three_words_response(utils.clean_text_answer(answer_for_scoring)) |
|
display_answer = user_answer_raw |
|
elif "day of today's date" in q_data["question"]: |
|
normalized_day = utils.normalize_date_answer(answer_for_scoring) |
|
point = 1 if normalized_day == str(now.day) else 0 |
|
display_answer = user_answer_raw |
|
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 |
|
display_answer = user_answer_raw |
|
else: |
|
point = utils.score_keyword_match(q_data["answer"], utils.clean_text_answer(normalized_answer)) |
|
if point == 1: |
|
point = q_data["max_points"] |
|
display_answer = user_answer_raw |
|
|
|
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: '{display_answer}' | Expected: '{q_data['answer']}'") |
|
results.append(result_string) |
|
total_score += point |
|
total_possible_score += q_data["max_points"] |
|
|
|
|
|
return "\n\n".join(results), f"{total_score} / {total_possible_score}" |