# app_AD2class.py # Main application file. Builds a tabbed Gradio UI for multiple assessments. import gradio as gr from datetime import datetime import time import pyminizip # Use pyminizip for encryption # import pyzipper as zipfile import os # Import the separated modules import MMSE import CDRM import utils # --- Classification Lookup Table --- # Based on the provided table: Rows=CD Memory Rating, Cols=MMSE Score CLASSIFICATION_TABLE = { 0.0: {**{i: "CN" for i in range(24, 31)}, **{i: "Not Applicable" for i in range(0, 24)}}, 0.5: {**{i: "MCI" for i in range(24, 31)}, **{i: "AD" for i in range(21, 24)}, **{i: "Not Applicable" for i in range(0, 21)}}, 1.0: {**{i: "MCI" for i in range(27, 31)}, **{i: "AD" for i in range(10, 27)}, **{i: "Not Applicable" for i in range(0, 10)}}, 2.0: {i: "AD" for i in range(0, 31)}, 3.0: {i: "AD" for i in range(0, 31)} } # --- ATN Score Lookup Table --- # Key: (Amyloid Beta 42, Phosphor-Tau217, Total Tau) ATN_LOOKUP_TABLE = { (1, 1, 1): 1, (1, 1, 0): 2, (1, 0, 1): 3, (1, 0, 0): 4, (0, 1, 1): 5, (0, 1, 0): 6, (0, 0, 1): 7, (0, 0, 0): 8, } # --- Session Info Tab UI --- def build_session_info_tab( patient_name_s, patient_id_s, dob_s, education_s, clinician_name_s, caregiver_name_s, assessment_date_s ): """Builds the UI for entering session and patient information.""" with gr.Blocks() as session_tab: gr.Markdown("## Session Information") gr.Markdown("Enter patient and session details below. This information will be included in the final report.") with gr.Row(): with gr.Column(): gr.Markdown("### Patient Details") patient_name_input = gr.Textbox(label="Patient Name", placeholder="First Name, Last Name") patient_id_input = gr.Textbox(label="Patient ID / MRN", placeholder="e.g., 123456789") dob_input = gr.Textbox(label="Date of Birth", placeholder="YYYY-MM-DD") education_input = gr.Number(label="Years of Education", value=12, minimum=0, step=1) with gr.Column(): gr.Markdown("### Assessment Details") assessment_date_input = gr.Textbox(label="Date of Assessment", value=datetime.now().strftime('%Y-%m-%d')) clinician_name_input = gr.Textbox(label="Clinician Name") caregiver_name_input = gr.Textbox(label="Caregiver Name (if present)") # Link all inputs to their respective state variables patient_name_input.change(lambda x: x, inputs=patient_name_input, outputs=patient_name_s) patient_id_input.change(lambda x: x, inputs=patient_id_input, outputs=patient_id_s) dob_input.change(lambda x: x, inputs=dob_input, outputs=dob_s) education_input.change(lambda x: x, inputs=education_input, outputs=education_s) assessment_date_input.change(lambda x: x, inputs=assessment_date_input, outputs=assessment_date_s) clinician_name_input.change(lambda x: x, inputs=clinician_name_input, outputs=clinician_name_s) caregiver_name_input.change(lambda x: x, inputs=caregiver_name_input, outputs=caregiver_name_s) return session_tab # --- MMSE UI and Logic --- def build_mmse_tab(mmse_score_state, language_state, mmse_answers_state): """Builds the UI components for the MMSE tab.""" with gr.Blocks() as mmse_tab: gr.Markdown("## Mini Mental State Exam") question_index = gr.State(0) answers = mmse_answers_state jump_nav = gr.Dropdown(choices=MMSE.QUESTION_CHOICES, value=MMSE.QUESTION_CHOICES[0], label="Jump to Question") initial_q = MMSE.STRUCTURED_QUESTIONS[0] progress_text = gr.Markdown(f"## {initial_q['main_cat']} - Q{initial_q['main_num']}{initial_q['sub_letter']} ({1} of {MMSE.TOTAL_QUESTIONS})") instruction_display = gr.Markdown(initial_q["instruction"]) question_button = gr.Button(f"Say πŸ”Š {initial_q['question']}", variant="secondary") with gr.Group(): answer_text = gr.Textbox(label="Your Answer (type or record below)") audio_input = gr.Audio(sources=["microphone"], label="🎀 Record Your Answer Here") image_upload = gr.Image(type="filepath", label="Upload Drawing for Drawing Question", visible=(initial_q['main_num'] == 11)) with gr.Row(): prev_btn = gr.Button("⬅️ Previous Question") next_btn = gr.Button("Next Question ➑️") tts_audio = gr.Audio(autoplay=True, visible=False) score_lines = gr.Textbox(label="Score by Question", lines=15, interactive=False) total = gr.Textbox(label="Total Score", interactive=False) submit_btn = gr.Button("βœ… Submit MMSE Answers") start_over_btn = gr.Button("πŸ”„ Start Over", visible=False) def process_evaluation_MMSE(answers_list, image_upload): score_lines_out, total_out = MMSE.evaluate_MMSE(answers_list, image_upload) return score_lines_out, total_out, gr.update(visible=False), gr.update(visible=True), total_out audio_input.change(utils.transcribe, inputs=[audio_input, language_state], outputs=answer_text) question_button.click(MMSE.speak_question, inputs=question_index, outputs=tts_audio) outputs_list = [question_button, answer_text, question_index, progress_text, instruction_display, jump_nav, image_upload, audio_input, answers] next_btn.click(lambda *args: utils.save_and_navigate("next", *args, module=MMSE), inputs=[question_index, answer_text, answers], outputs=outputs_list) prev_btn.click(lambda *args: utils.save_and_navigate("prev", *args, module=MMSE), inputs=[question_index, answer_text, answers], outputs=outputs_list) jump_nav.change(lambda *args: utils.jump_to_question(*args, module=MMSE), inputs=[jump_nav, question_index, answer_text, answers], outputs=outputs_list) submit_btn.click( utils.save_final_answer, inputs=[question_index, answer_text, answers], outputs=answers ).then( fn=process_evaluation_MMSE, inputs=[answers, image_upload], outputs=[score_lines, total, submit_btn, start_over_btn, mmse_score_state] ) start_over_btn.click(lambda: utils.reset_app(MMSE), outputs=[ question_index, answers, score_lines, total, question_button, progress_text, instruction_display, answer_text, jump_nav, audio_input, image_upload, start_over_btn, submit_btn, tts_audio, mmse_score_state ]) return mmse_tab # --- CDRM UI and Logic --- def build_cdrm_tab(cdrm_score_state, language_state): """Builds the fully functional UI components for the CDR Memory tab.""" with gr.Blocks() as cdrm_tab: gr.Markdown("## CDR Memory Assessment") question_index = gr.State(0) answers = gr.State([""] * CDRM.TOTAL_QUESTIONS) jump_nav = gr.Dropdown(choices=CDRM.QUESTION_CHOICES, value=CDRM.QUESTION_CHOICES[0], label="Jump to Question") initial_q = CDRM.STRUCTURED_QUESTIONS[0] progress_text = gr.Markdown(f"## {initial_q['main_cat']} - Q{initial_q['main_num']}{initial_q['sub_letter']} ({1} of {CDRM.TOTAL_QUESTIONS})") instruction_display = gr.Markdown(initial_q["instruction"]) question_button = gr.Button(f"Say πŸ”Š {initial_q['question']}", variant="secondary") with gr.Group(): answer_text = gr.Textbox(label="Your Answer (type or record below)") audio_input = gr.Audio(sources=["microphone"], label="🎀 Record Your Answer Here") image_upload = gr.Image(type="filepath", label="Upload Drawing", visible=("draw a copy" in initial_q["question"])) with gr.Row(): prev_btn = gr.Button("⬅️ Previous Question") next_btn = gr.Button("Next Question ➑️") tts_audio = gr.Audio(autoplay=True, visible=False) score_lines = gr.Textbox(label="Score by Question", lines=15, interactive=False) total = gr.Textbox(label="Total Score", interactive=False) submit_btn = gr.Button("βœ… Submit CDRM Answers") start_over_btn = gr.Button("πŸ”„ Start Over", visible=False) def process_evaluation_CDRM(answers_list): score_lines_out, total_out = CDRM.evaluate_CDRM(answers_list) return score_lines_out, total_out, gr.update(visible=False), gr.update(visible=True), total_out audio_input.change(utils.transcribe, inputs=[audio_input, language_state], outputs=answer_text) question_button.click(CDRM.speak_question, inputs=question_index, outputs=tts_audio) outputs_list = [question_button, answer_text, question_index, progress_text, instruction_display, jump_nav, image_upload, audio_input, answers] next_btn.click(lambda *args: utils.save_and_navigate("next", *args, module=CDRM), inputs=[question_index, answer_text, answers], outputs=outputs_list) prev_btn.click(lambda *args: utils.save_and_navigate("prev", *args, module=CDRM), inputs=[question_index, answer_text, answers], outputs=outputs_list) jump_nav.change(lambda *args: utils.jump_to_question(*args, module=CDRM), inputs=[jump_nav, question_index, answer_text, answers], outputs=outputs_list) submit_btn.click( utils.save_final_answer, inputs=[question_index, answer_text, answers], outputs=answers ).then( fn=process_evaluation_CDRM, inputs=[answers], outputs=[score_lines, total, submit_btn, start_over_btn, cdrm_score_state] ) start_over_btn.click(lambda: utils.reset_app(CDRM), outputs=[ question_index, answers, score_lines, total, question_button, progress_text, instruction_display, answer_text, jump_nav, audio_input, image_upload, start_over_btn, submit_btn, tts_audio, cdrm_score_state ]) return cdrm_tab # --- Biomarkers UI and Logic --- def build_biomarkers_tab(abeta42_s, abeta40_s, ptau217_s, ttau_s, initial_atn_state): """Builds the UI for inputting and calculating biomarker data.""" with gr.Blocks() as biomarkers_tab: gr.Markdown("## Biomarker Input") gr.Markdown("Select 0 or 1 for each biomarker. The ATN score will be calculated automatically.") with gr.Row(): abeta42_input = gr.Radio(choices=[0, 1], value=0, label="Amyloid Beta 42 (A)") abeta40_input = gr.Radio(choices=[0, 1], value=0, label="Amyloid Beta 40") with gr.Row(): ptau217_input = gr.Radio(choices=[0, 1], value=0, label="Phosphor-tau217 (T)") ttau_input = gr.Radio(choices=[0, 1], value=0, label="Total Tau (N)") atn_score_display = gr.Textbox(label="ATN Biomarkers Multinomial Score", interactive=False, value=8) save_btn = gr.Button("Save Biomarker Data") save_confirmation = gr.Markdown("") def calculate_atn_score(amyloid_42, ptau_217, total_tau): key = (int(amyloid_42), int(ptau_217), int(total_tau)) score = ATN_LOOKUP_TABLE.get(key, "Invalid") return score atn_inputs = [abeta42_input, ptau217_input, ttau_input] for component in atn_inputs: component.change(fn=calculate_atn_score, inputs=atn_inputs, outputs=atn_score_display) def save_biomarker_data(ab42, ab40, pt217, tt, atn_score): atn_score_str = str(atn_score) return { abeta42_s: ab42, abeta40_s: ab40, ptau217_s: pt217, ttau_s: tt, initial_atn_state: atn_score_str, save_confirmation: f"βœ… Biomarker data saved. ATN Score {atn_score} is now available in the Joint Analysis tab." } save_btn.click(fn=save_biomarker_data, inputs=[abeta42_input, abeta40_input, ptau217_input, ttau_input, atn_score_display], outputs=[abeta42_s, abeta40_s, ptau217_s, ttau_s, initial_atn_state, save_confirmation]) return biomarkers_tab # --- Joint Analysis UI --- def build_joint_analysis_tab( initial_mmse_state, initial_cdr_state, initial_atn_state, final_mmse_state, final_cdr_state, final_atn_state, mmse_remarks_state, cdr_remarks_state, atn_remarks_state, session_info_states, mmse_answers_state, biomarker_states, language_state, report_password_state ): with gr.Blocks() as joint_tab: gr.Markdown("## Joint Analysis") gr.Markdown("This tab uses the results from the assessments and a selected biomarker profile for a combined analysis.") gr.Markdown("### Initial Scores from Assessments") with gr.Row(): initial_mmse_display = gr.Textbox(label="MMSE Score", value="30 / 30", interactive=False) initial_cdr_display = gr.Textbox(label="CD Memory Rating", value="0 / 3", interactive=False) initial_atn_display = gr.Textbox(label="ATN Biomarkers' Score", value="8", interactive=False) gr.Markdown("### Finalized with Expert Judgment") with gr.Row(): mmse_input = gr.Dropdown(choices=[str(i) for i in range(30, -1, -1)], value="30", label="MMSE Score") cdr_input = gr.Dropdown(choices=["0", "0.5", "1", "2", "3"], value="0", label="CD Memory Rating") atn_input = gr.Dropdown(choices=[str(i) for i in range(8, 0, -1)], value="8", label="ATN Biomarkers' Score") gr.Markdown("### Justification & Remarks by Clinician") with gr.Row(): mmse_remarks = gr.Textbox(label="MMSE Notes", lines=4, placeholder="Enter justification for MMSE score change...") cdr_remarks = gr.Textbox(label="CD Memory Notes", lines=4, placeholder="Enter justification for CDR rating change...") atn_remarks = gr.Textbox(label="ATN Biomarkers Notes", lines=4, placeholder="Enter justification for ATN score selection...") report_password_input = gr.Textbox(label="Create Password for Report (Optional)", type="password", placeholder="Leave blank for unencrypted .txt report") with gr.Row(): analyze_btn = gr.Button("Analyze Joint Data") save_report_btn = gr.Button("Save Full Report") outcome_display = gr.Textbox(label="Combined Analysis Result", interactive=False, lines=12) download_file = gr.File(label="Download Report", visible=False) def analyze_outcomes(mmse_score, cdr_rating, atn_score, mmse_note, cdr_note, atn_note): try: mmse_int = int(mmse_score) cdr_float = float(cdr_rating) classification = CLASSIFICATION_TABLE.get(cdr_float, {}).get(mmse_int, "Not Applicable") except (ValueError, TypeError): classification = "Invalid Score Input" legend = ("--- CLASSIFICATION LEGEND ---\n" " - AD: Alzheimer’s Disease\n - MCI: Mild Cognitive Impairment\n - CN: Cognitive Normal") analysis_text = (f"--- DEMENTIA CLASSIFICATION ---\n - Outcome: {classification}\n\n" f"--- FINALIZED SCORES ---\n - MMSE Score: {mmse_score}\n - CD Memory Rating: {cdr_rating}\n - ATN Biomarkers' Score: {atn_score}\n\n" f"--- CLINICIAN REMARKS ---\n - MMSE Notes: {mmse_note or 'N/A'}\n - CD Memory Notes: {cdr_note or 'N/A'}\n - ATN Biomarkers Notes: {atn_note or 'N/A'}\n\n" f"{legend}") return analysis_text def generate_report( final_mmse, final_cdr, final_atn, mmse_notes, cdr_notes, atn_notes, p_name, p_id, p_dob, p_edu, c_name, cg_name, a_date, mmse_answers, mmse_total_score, ab42, ab40, pt217, tt, language_code, password ): try: mmse_int = int(final_mmse) cdr_float = float(final_cdr) classification = CLASSIFICATION_TABLE.get(cdr_float, {}).get(mmse_int, "Not Applicable") except (ValueError, TypeError): classification = "Invalid Score Input" mmse_details = [f" - Q{q['main_num']}{q['sub_letter']}: {q['question']}\n - Answer: {mmse_answers[i] or 'No answer'}" for i, q in enumerate(MMSE.STRUCTURED_QUESTIONS)] mmse_details_str = "\n".join(mmse_details) report_content = (f"=========================================\nCognitive & Biomarker Assessment Report\n=========================================\n\n" f"--- SESSION INFORMATION ---\n" f"Date of Assessment: {a_date or 'N/A'}\nPatient Name: {p_name or 'N/A'}\nPatient ID/MRN: {p_id or 'N/A'}\n" f"Date of Birth: {p_dob or 'N/A'}\nYears of Education: {p_edu or 'N/A'}\nClinician Name: {c_name or 'N/A'}\nCaregiver Name: {cg_name or 'N/A'}\n\n" f"--- JOINT ANALYSIS ---\n" f"Dementia Classification: {classification}\nFinal MMSE Score: {final_mmse}\nFinal CD Memory Rating: {final_cdr}\nFinal ATN Score: {final_atn}\n" f"MMSE Notes: {mmse_notes or 'N/A'}\nCD Memory Notes: {cdr_notes or 'N/A'}\nATN Biomarkers Notes: {atn_notes or 'N/A'}\n\n" f"--- MMSE RESULTS ---\n" f"Total Score: {mmse_total_score or 'Not completed'}\n{mmse_details_str}\n\n" f"--- BIOMARKER INPUTS ---\n" f"Amyloid Beta 42 (A): {ab42}\nPhosphor-tau217 (T): {pt217}\nTotal Tau (N): {tt}\nAmyloid Beta 40: {ab40}\n\n" f"--- SETTINGS ---\n" f"Language Used for Recording: {language_code.upper()}\n") timestamp = int(time.time()) # --- CORRECTED CODE BLOCK --- if password: temp_txt_path = os.path.join("/tmp", f"report_{timestamp}.txt") zip_filepath = os.path.join("/tmp", f"report_encrypted_{timestamp}.zip") with open(temp_txt_path, "w", encoding="utf-8") as f: f.write(report_content) # Use pyminizip for robust, simple encryption pyminizip.compress(temp_txt_path, None, zip_filepath, password, 9) os.remove(temp_txt_path) # Clean up the unencrypted temp file return zip_filepath, gr.update(visible=True) else: txt_filepath = os.path.join("/tmp", f"report_{timestamp}.txt") with open(txt_filepath, "w", encoding="utf-8") as f: f.write(report_content) return txt_filepath, gr.update(visible=True) initial_mmse_state.change(lambda x: x or "30 / 30", inputs=initial_mmse_state, outputs=initial_mmse_display) initial_cdr_state.change(lambda x: x or "0 / 3", inputs=initial_cdr_state, outputs=initial_cdr_display) initial_atn_state.change(lambda x: x or "8", inputs=initial_atn_state, outputs=initial_atn_display) mmse_input.change(lambda x: x, inputs=mmse_input, outputs=final_mmse_state) cdr_input.change(lambda x: x, inputs=cdr_input, outputs=final_cdr_state) atn_input.change(lambda x: x, inputs=atn_input, outputs=final_atn_state) mmse_remarks.change(lambda x: x, inputs=mmse_remarks, outputs=mmse_remarks_state) cdr_remarks.change(lambda x: x, inputs=cdr_remarks, outputs=cdr_remarks_state) atn_remarks.change(lambda x: x, inputs=atn_remarks, outputs=atn_remarks_state) report_password_input.change(lambda x: x, inputs=report_password_input, outputs=report_password_state) analysis_inputs = [final_mmse_state, final_cdr_state, final_atn_state, mmse_remarks_state, cdr_remarks_state, atn_remarks_state] analyze_btn.click(analyze_outcomes, inputs=analysis_inputs, outputs=outcome_display) report_inputs = analysis_inputs + session_info_states + [mmse_answers_state, initial_mmse_state] + biomarker_states + [language_state, report_password_state] save_report_btn.click(generate_report, inputs=report_inputs, outputs=[download_file, download_file]) return joint_tab # --- Settings Tab UI --- def build_settings_tab(language_state): """Builds the UI for the application settings, like language selection.""" with gr.Blocks() as settings_tab: gr.Markdown("## Settings") gr.Markdown("### Language for Voice Recording") language_map = { "English": "en", "Chinese": "zh", "Malay": "ms", "Tamil": "ta" } lang_selection = gr.Radio(choices=list(language_map.keys()), value="English", label="Select the language the patient will be speaking") save_status = gr.Markdown("") def update_language_setting(selected_language): lang_code = language_map[selected_language] return lang_code, f"βœ… Language set to **{selected_language}**." lang_selection.change(update_language_setting, inputs=lang_selection, outputs=[language_state, save_status]) return settings_tab # --- Main Application Build --- def build_ui(): """Builds the main Gradio interface with tabs.""" with gr.Blocks(theme=gr.themes.Soft(), title="Cognitive Assessment") as demo: gr.Markdown("# Clinical Dementia Staging Assessment") # gr.Markdown("# DIAGNOSTIC TEST - VERSION 2") patient_name_state = gr.State("") patient_id_state = gr.State("") dob_state = gr.State("") education_state = gr.State(12) clinician_name_state = gr.State("") caregiver_name_state = gr.State("") assessment_date_state = gr.State(datetime.now().strftime('%Y-%m-%d')) session_info_states = [patient_name_state, patient_id_state, dob_state, education_state, clinician_name_state, caregiver_name_state, assessment_date_state] language_state = gr.State("en") mmse_score_state = gr.State("") cdrm_score_state = gr.State("") mmse_answers_state = gr.State([""] * MMSE.TOTAL_QUESTIONS) abeta42_state = gr.State(0) abeta40_state = gr.State(0) ptau217_state = gr.State(0) ttau_state = gr.State(0) biomarker_states = [abeta42_state, abeta40_state, ptau217_state, ttau_state] initial_atn_state = gr.State("8") final_mmse_state = gr.State("30") final_cdr_state = gr.State("0") final_atn_state = gr.State("8") mmse_remarks_state = gr.State("") cdr_remarks_state = gr.State("") atn_remarks_state = gr.State("") report_password_state = gr.State("") with gr.Tabs(): with gr.TabItem("Session Info"): build_session_info_tab(*session_info_states) with gr.TabItem("Joint Analysis"): build_joint_analysis_tab( mmse_score_state, cdrm_score_state, initial_atn_state, final_mmse_state, final_cdr_state, final_atn_state, mmse_remarks_state, cdr_remarks_state, atn_remarks_state, session_info_states, mmse_answers_state, biomarker_states, language_state, report_password_state ) with gr.TabItem("MMSE"): build_mmse_tab(mmse_score_state, language_state, mmse_answers_state) with gr.TabItem("CDR Memory"): build_cdrm_tab(cdrm_score_state, language_state) with gr.TabItem("Biomarkers"): build_biomarkers_tab( abeta42_state, abeta40_state, ptau217_state, ttau_state, initial_atn_state ) with gr.TabItem("Settings"): build_settings_tab(language_state) return demo if __name__ == "__main__": MMSE.pregenerate_audio() CDRM.pregenerate_audio() app = build_ui() app.launch()