""" Planning Summary Audio Analyzer - Hugging Face Spaces App Analyzes audio recordings of planning conversations and generates a structured Word document planning summary report using Google's Gemini API. """ import os import re import json import time import tempfile import gradio as gr import google.generativeai as genai from docx import Document from docx.shared import Inches, Pt, Twips from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.style import WD_STYLE_TYPE from docx.oxml.ns import qn from docx.oxml import OxmlElement # ============================================================================ # EXTRACTION PROMPT # ============================================================================ EXTRACTION_PROMPT = """ You are analyzing a recorded conversation about advance care planning and end-of-life wishes. Listen to the ENTIRE audio carefully and extract ALL relevant information. CRITICAL INSTRUCTIONS FOR SINGLE-SELECT FIELDS: - You MUST select exactly ONE option for each single-select field - Use the EXACT string values specified (copy them exactly) - If the conversation implies something even indirectly, make your best inference - NEVER leave single-select fields as null - always pick the best match Return a JSON object with this EXACT structure: ```json { "participant": { "name": "First and last name if mentioned", "conversation_date": "MM/DD/YYYY format if mentioned", "facilitator": "Facilitator name and credentials", "location": "Location of conversation" }, "health_care_wishes": { "primary_decision_maker": { "name": "Name", "relationship": "spouse/wife/husband/son/daughter/etc", "phone": "phone or null", "email": "email or null" }, "backup_decision_maker": { "name": "Name", "relationship": "relationship", "phone": "phone or null" }, "values_summary": "2-3 sentence summary of their values", "advance_care_status": "MUST BE ONE OF: has_current_documents | has_documents_needs_update | no_documents", "treatment_preference": "MUST BE ONE OF: comfort_care_only | full_treatment_if_recovery | unsure", "treatment_details": "Details about CPR, ventilation, ICU preferences", "additional_notes": "Other notes or null" }, "financial_planning": { "financial_primary": { "name": "Name", "relationship": "relationship", "phone": "phone or null", "email": "email or null" }, "financial_backup": { "name": "Name", "relationship": "relationship", "phone": "phone or null" }, "financial_conversation_summary": "Summary of who handles finances", "documents_in_place": { "financial_poa": false, "will": false, "living_trust": false, "tod_designations": false, "joint_ownership": false, "none": true }, "legal_details": "Details about their legal/financial situation", "next_steps": { "review_update_poa_will": false, "identify_alternate": false, "contact_attorney": true, "seek_trust_advice": false, "other": "Other specific steps mentioned or null" }, "beneficiary_status": "MUST BE ONE OF: all_current | need_to_update | unsure", "account_notes": "Notes about 401k, pension, retirement accounts", "beneficiary_conversation": "Summary of beneficiary discussion", "has_info_list": "MUST BE ONE OF: yes_shared | yes_not_shared | not_created", "info_location": "Where files/info are stored", "organization_ideas": "Ideas about organizing documents", "shared_with_loved_ones": "MUST BE ONE OF: yes_written | yes_not_written | not_yet", "sharing_notes": "Notes about family discussions", "overall_wishes": "Summary of financial wishes", "has_specific_items": true, "specific_items": [ {"item": "Fishing gear", "recipient": "Son Charlie"}, {"item": "Guitar", "recipient": "Daughter Betsy"} ] }, "funeral_plans": { "service_type": "MUST BE ONE OF: funeral | memorial | celebration_of_life | other", "service_type_other": "If other, describe", "body_preference": "MUST BE ONE OF: burial | cremation | donation | undecided", "body_details": "Location details like 'Ashes spread at favorite fishing lake'", "conversation_summary": "Summary of funeral wishes", "preferred_location": "Where service should be held", "service_leader": "Who should lead", "music_readings": "Music and reading preferences", "appearance_clothing": "Dress code preferences", "charity_donations": "Charity for donations", "cost_planning": "MUST BE ONE OF: prepaid | family_aware | needs_discussion", "additional_notes": "Notes about life insurance, costs, etc." }, "values_reflections": { "what_matters_most": "How they want to be remembered", "meaning_and_joy": "What gives their life meaning", "want_remembered_for": "What they hope people remember" }, "facilitator_summary": "Facilitator's closing summary and recommendations" } ``` DECISION GUIDE FOR COMMON SCENARIOS: advance_care_status: - If they say they haven't done paperwork/documents yet -> "no_documents" - If they have old documents that need updating -> "has_documents_needs_update" - If they have current, up-to-date documents -> "has_current_documents" treatment_preference: - If they want treatment/CPR/ventilation IF there's hope of recovery -> "full_treatment_if_recovery" - If they only want comfort/palliative care, no machines -> "comfort_care_only" - If they're unsure or need more information -> "unsure" beneficiary_status: - If they're not sure who's listed or need to check -> "unsure" - If they know some need updating -> "need_to_update" - If everything is current -> "all_current" has_info_list: - If they have files/info but haven't shared location or it's disorganized -> "yes_not_shared" - If their trusted person knows where everything is -> "yes_shared" - If they haven't created any list -> "not_created" shared_with_loved_ones: - If they've talked but nothing written down -> "yes_not_written" - If they've discussed AND written it down -> "yes_written" - If they haven't discussed wishes yet -> "not_yet" service_type: - Celebration of life, casual gathering, party -> "celebration_of_life" - Traditional funeral -> "funeral" - Memorial service -> "memorial" body_preference: - Cremation, ashes spread somewhere -> "cremation" - Burial in cemetery/ground -> "burial" - Donate body to science -> "donation" cost_planning: - If they mention life insurance will cover it or family knows about funding -> "family_aware" - If they have a pre-paid funeral plan -> "prepaid" - If costs haven't been discussed -> "needs_discussion" Listen for these key topics: - Who would make healthcare decisions (usually spouse first, then adult child) - Who would handle finances (often same people) - Whether they have existing legal documents - Their wishes about medical treatment and life support - Funeral/memorial preferences - Special items to give specific people - What matters most to them, their values Return ONLY valid JSON, no markdown formatting or explanation. """ # ============================================================================ # AUDIO ANALYSIS # ============================================================================ def analyze_audio(audio_path: str, api_key: str) -> str: """Upload audio to Gemini and extract planning information.""" genai.configure(api_key=api_key) audio_file = genai.upload_file(audio_path) # Wait for processing while audio_file.state.name == "PROCESSING": time.sleep(5) audio_file = genai.get_file(audio_file.name) if audio_file.state.name == "FAILED": raise ValueError(f"Audio processing failed: {audio_file.state.name}") model = genai.GenerativeModel('gemini-2.0-flash-exp') response = model.generate_content( [audio_file, EXTRACTION_PROMPT], generation_config=genai.GenerationConfig( temperature=0.1, max_output_tokens=8192 ) ) # Cleanup uploaded file genai.delete_file(audio_file.name) return response.text # ============================================================================ # JSON PARSING AND NORMALIZATION # ============================================================================ def parse_json_response(response_text: str) -> dict | None: """Extract JSON from Gemini response.""" cleaned = re.sub(r'```json\s*', '', response_text) cleaned = re.sub(r'```\s*', '', cleaned) json_match = re.search(r'\{[\s\S]*\}', cleaned) if json_match: try: return json.loads(json_match.group()) except json.JSONDecodeError: return None return None def normalize_value(value, valid_options, default=None): """Normalize a value to match one of the valid options.""" if value is None: return default val_str = str(value).lower().strip() val_normalized = val_str.replace(' ', '_').replace('-', '_') # Direct match for opt in valid_options: if val_normalized == opt.lower(): return opt # Fuzzy matching rules matching_rules = { 'no_documents': ['no_documents', 'none', 'not_completed', 'no documents', 'not yet'], 'has_current_documents': ['has_current', 'current', 'up_to_date', 'have documents'], 'has_documents_needs_update': ['needs_update', 'need_update', 'review', 'outdated'], 'full_treatment_if_recovery': ['full_treatment', 'full treatment', 'aggressive', 'treatment if recovery'], 'comfort_care_only': ['comfort', 'comfort_care', 'palliative', 'no machines'], 'unsure': ['unsure', 'not sure', 'uncertain', 'undecided', 'need more info'], 'need_to_update': ['need_to_update', 'needs update', 'update', 'outdated'], 'all_current': ['all_current', 'current', 'up to date', 'all updated'], 'yes_not_shared': ['yes_not_shared', 'yes but', 'have but', 'not shared', 'disorganized'], 'yes_shared': ['yes_shared', 'yes shared', 'knows where', 'shared'], 'not_created': ['not_created', 'no', 'none', "haven't created", 'not yet'], 'yes_not_written': ['yes_not_written', 'discussed not written', 'talked but', 'verbal', 'not written'], 'yes_written': ['yes_written', 'written', 'documented', 'written down'], 'not_yet': ['not_yet', 'no', "haven't", 'not discussed'], 'celebration_of_life': ['celebration', 'celebration_of_life', 'party', 'gathering', 'casual'], 'funeral': ['funeral', 'traditional'], 'memorial': ['memorial', 'memorial_service'], 'other': ['other'], 'cremation': ['cremation', 'cremate', 'ashes', 'cremated'], 'burial': ['burial', 'bury', 'buried', 'cemetery', 'ground'], 'donation': ['donation', 'donate', 'science', 'donate body'], 'undecided': ['undecided', 'unsure', 'not sure'], 'family_aware': ['family_aware', 'family aware', 'life insurance', 'insurance', 'covered'], 'prepaid': ['prepaid', 'pre-paid', 'pre paid', 'paid'], 'needs_discussion': ['needs_discussion', 'need to discuss', 'not discussed'] } for opt in valid_options: if opt in matching_rules: for pattern in matching_rules[opt]: if pattern in val_str or pattern in val_normalized: return opt return default def normalize_data(data: dict) -> dict: """Normalize all single-select fields in the extracted data.""" if not data: return data # Health care wishes if 'health_care_wishes' in data: hcw = data['health_care_wishes'] hcw['advance_care_status'] = normalize_value( hcw.get('advance_care_status'), ['has_current_documents', 'has_documents_needs_update', 'no_documents'], 'no_documents' ) hcw['treatment_preference'] = normalize_value( hcw.get('treatment_preference'), ['comfort_care_only', 'full_treatment_if_recovery', 'unsure'], 'full_treatment_if_recovery' ) # Financial planning if 'financial_planning' in data: fp = data['financial_planning'] fp['beneficiary_status'] = normalize_value( fp.get('beneficiary_status'), ['all_current', 'need_to_update', 'unsure'], 'unsure' ) fp['has_info_list'] = normalize_value( fp.get('has_info_list'), ['yes_shared', 'yes_not_shared', 'not_created'], 'yes_not_shared' ) fp['shared_with_loved_ones'] = normalize_value( fp.get('shared_with_loved_ones'), ['yes_written', 'yes_not_written', 'not_yet'], 'yes_not_written' ) # Funeral plans if 'funeral_plans' in data: fun = data['funeral_plans'] fun['service_type'] = normalize_value( fun.get('service_type'), ['funeral', 'memorial', 'celebration_of_life', 'other'], 'celebration_of_life' ) fun['body_preference'] = normalize_value( fun.get('body_preference'), ['burial', 'cremation', 'donation', 'undecided'], 'cremation' ) fun['cost_planning'] = normalize_value( fun.get('cost_planning'), ['prepaid', 'family_aware', 'needs_discussion'], 'family_aware' ) return data # ============================================================================ # WORD DOCUMENT GENERATION # ============================================================================ def get_value(data, *keys, default="Not discussed"): """Safely get nested dictionary values.""" result = data for key in keys: if isinstance(result, dict) and key in result: result = result[key] else: return default if result is None: return default return result if result else default def cb(checked: bool) -> str: """Return Unicode checkbox.""" return "\u2612" if checked else "\u2610" def is_true(val) -> bool: """Check if a value is truthy.""" if val is None: return False if isinstance(val, bool): return val if isinstance(val, str): return val.lower() in ('true', 'yes', '1') return bool(val) def set_cell_shading(cell, color): """Set cell background color.""" shading_elm = OxmlElement('w:shd') shading_elm.set(qn('w:fill'), color) cell._tc.get_or_add_tcPr().append(shading_elm) def add_checkbox_paragraph(doc, checked, text, indent_level=0): """Add a paragraph with checkbox.""" p = doc.add_paragraph() p.paragraph_format.left_indent = Inches(0.25 * indent_level) p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) run = p.add_run(f"{cb(checked)} {text}") run.font.name = 'Arial' run.font.size = Pt(10) return p def add_field_label(doc, text): """Add a field label paragraph.""" p = doc.add_paragraph() p.paragraph_format.space_before = Pt(8) p.paragraph_format.space_after = Pt(2) run = p.add_run(text) run.font.name = 'Arial' run.font.size = Pt(10) run.bold = True return p def add_field_value(doc, text): """Add a field value paragraph.""" p = doc.add_paragraph() p.paragraph_format.space_after = Pt(4) run = p.add_run(text) run.font.name = 'Arial' run.font.size = Pt(10) return p def add_section_header(doc, text): """Add a section header.""" p = doc.add_paragraph() p.paragraph_format.space_before = Pt(16) p.paragraph_format.space_after = Pt(8) run = p.add_run(text) run.font.name = 'Arial' run.font.size = Pt(14) run.bold = True return p def add_sub_header(doc, text): """Add a sub-header.""" p = doc.add_paragraph() p.paragraph_format.space_before = Pt(12) p.paragraph_format.space_after = Pt(4) run = p.add_run(text) run.font.name = 'Arial' run.font.size = Pt(11) run.bold = True return p def generate_docx(data: dict, output_path: str) -> str: """Generate the planning summary Word document.""" doc = Document() # Set default font style = doc.styles['Normal'] style.font.name = 'Arial' style.font.size = Pt(10) # Set page margins (0.75 inch) for section in doc.sections: section.top_margin = Inches(0.6) section.bottom_margin = Inches(0.6) section.left_margin = Inches(0.75) section.right_margin = Inches(0.75) # ===== TITLE ===== title = doc.add_paragraph() title.alignment = WD_ALIGN_PARAGRAPH.CENTER title_run = title.add_run("My Planning Summary") title_run.font.name = 'Arial' title_run.font.size = Pt(16) title_run.bold = True participant = data.get('participant', {}) # Participant info p = doc.add_paragraph() p.add_run("Participant Name: ").bold = True p.add_run(get_value(participant, 'name')) p = doc.add_paragraph() p.add_run("Date of Conversation: ").bold = True p.add_run(get_value(participant, 'conversation_date')) p = doc.add_paragraph() p.add_run("Facilitator: ").bold = True p.add_run(get_value(participant, 'facilitator')) p = doc.add_paragraph() p.add_run("Location: ").bold = True p.add_run(get_value(participant, 'location')) # Intro text intro = doc.add_paragraph() intro.paragraph_format.space_before = Pt(8) intro.paragraph_format.space_after = Pt(12) intro_run = intro.add_run( "This summary captures the main points of our conversation about planning for the future. " "It is not a legal document, but it can help you share your wishes and guide next steps." ) intro_run.font.size = Pt(9) intro_run.font.color.rgb = None # Use default color # ===== HEALTH & CARE WISHES ===== add_section_header(doc, "My Health & Care Wishes") health = data.get('health_care_wishes', {}) add_field_label(doc, "Who would you trust to make health decisions for you if you could not speak for yourself?") primary = health.get('primary_decision_maker', {}) add_field_value(doc, f"Primary: {get_value(primary, 'name')} – {get_value(primary, 'relationship')}") add_field_value(doc, f"Phone: {get_value(primary, 'phone')} | Email: {get_value(primary, 'email')}") backup = health.get('backup_decision_maker', {}) add_field_value(doc, f"Back-up: {get_value(backup, 'name')} – {get_value(backup, 'relationship')}") add_field_value(doc, f"Phone: {get_value(backup, 'phone')}") add_field_label(doc, "Summary of what you shared about your values and care priorities:") add_field_value(doc, get_value(health, 'values_summary')) add_field_label(doc, "Current advance-care planning status:") status = health.get('advance_care_status', '') add_checkbox_paragraph(doc, status == 'has_current_documents', "I have a health-care power of attorney or living will (up-to-date)") add_checkbox_paragraph(doc, status == 'has_documents_needs_update', "I have documents but need to review/update") add_checkbox_paragraph(doc, status == 'no_documents', "I have not yet completed these documents") add_field_label(doc, "Treatment preferences we discussed:") treatment = health.get('treatment_preference', '') add_checkbox_paragraph(doc, treatment == 'comfort_care_only', "Comfort care only (no machines or resuscitation if dying)") add_checkbox_paragraph(doc, treatment == 'full_treatment_if_recovery', "Full medical treatment if recovery possible") add_checkbox_paragraph(doc, treatment == 'unsure', "Unsure / would like more information") add_field_label(doc, "Additional details related to treatment preferences that were discussed:") add_field_value(doc, get_value(health, 'treatment_details')) add_field_label(doc, "Notes from conversation:") add_field_value(doc, get_value(health, 'additional_notes')) # ===== FINANCIAL PLANNING ===== doc.add_page_break() add_section_header(doc, "My Financial Planning & Legacy") financial = data.get('financial_planning', {}) add_sub_header(doc, "A. Trusted Person(s)") add_field_label(doc, "Who do you trust to manage your finances if you are unable?") fin_primary = financial.get('financial_primary', {}) add_field_value(doc, f"Primary: {get_value(fin_primary, 'name')} – {get_value(fin_primary, 'relationship')}") add_field_value(doc, f"Phone: {get_value(fin_primary, 'phone')} | Email: {get_value(fin_primary, 'email')}") fin_backup = financial.get('financial_backup', {}) add_field_value(doc, f"Back-up: {get_value(fin_backup, 'name')} – {get_value(fin_backup, 'relationship')}") add_field_value(doc, f"Phone: {get_value(fin_backup, 'phone')}") add_field_label(doc, "Conversation summary:") add_field_value(doc, get_value(financial, 'financial_conversation_summary')) add_sub_header(doc, "B. Legal and Financial Readiness") add_field_label(doc, "Documents currently in place (check all that apply):") docs = financial.get('documents_in_place', {}) add_checkbox_paragraph(doc, is_true(docs.get('financial_poa')), "Financial Power of Attorney (POA)") add_checkbox_paragraph(doc, is_true(docs.get('will')), "Will") add_checkbox_paragraph(doc, is_true(docs.get('living_trust')), "Living Trust") add_checkbox_paragraph(doc, is_true(docs.get('tod_designations')), "Transfer on Death (TOD) designations") add_checkbox_paragraph(doc, is_true(docs.get('joint_ownership')), "Joint ownership of key accounts") add_checkbox_paragraph(doc, is_true(docs.get('none')), "None completed yet") add_field_label(doc, "Details from conversation:") add_field_value(doc, get_value(financial, 'legal_details')) add_field_label(doc, "Next steps (as discussed):") next_steps = financial.get('next_steps', {}) add_checkbox_paragraph(doc, is_true(next_steps.get('review_update_poa_will')), "Review or update existing POA or Will") add_checkbox_paragraph(doc, is_true(next_steps.get('identify_alternate')), "Identify alternate decision-maker") add_checkbox_paragraph(doc, is_true(next_steps.get('contact_attorney')), "Contact attorney or legal aid for document preparation") add_checkbox_paragraph(doc, is_true(next_steps.get('seek_trust_advice')), "Seek advice on creating or updating a Trust") other_steps = next_steps.get('other') if other_steps and other_steps not in ('null', None, 'Not discussed', ''): add_checkbox_paragraph(doc, True, f"Other: {other_steps}") add_sub_header(doc, "C. Beneficiaries and Account Management") add_field_label(doc, "Status of major accounts and beneficiaries:") ben_status = financial.get('beneficiary_status', '') add_checkbox_paragraph(doc, ben_status == 'all_current', "All beneficiaries current and reflect my wishes") add_checkbox_paragraph(doc, ben_status == 'need_to_update', "Need to review or update some") add_checkbox_paragraph(doc, ben_status == 'unsure', "Unsure / need help locating information") add_field_label(doc, "Notes about specific accounts (bank, insurance, retirement):") add_field_value(doc, get_value(financial, 'account_notes')) add_field_label(doc, "Conversation summary:") add_field_value(doc, get_value(financial, 'beneficiary_conversation')) add_sub_header(doc, "D. Organizing Financial Information") add_field_label(doc, "Do you have a list of key information (accounts, passwords, insurance details)?") info_status = financial.get('has_info_list', '') add_checkbox_paragraph(doc, info_status == 'yes_shared', "Yes – my trusted person knows where it is") add_checkbox_paragraph(doc, info_status == 'yes_not_shared', "Yes – but not shared or outdated") add_checkbox_paragraph(doc, info_status == 'not_created', "Not yet created") add_field_label(doc, "Where this information can be found:") add_field_value(doc, get_value(financial, 'info_location')) add_field_label(doc, "Additional organization ideas shared in conversation:") add_field_value(doc, get_value(financial, 'organization_ideas')) add_sub_header(doc, "E. Talking with Loved Ones") add_field_label(doc, "Have you shared your financial wishes with loved ones or trusted helpers?") shared = financial.get('shared_with_loved_ones', '') add_checkbox_paragraph(doc, shared == 'yes_written', "Yes – discussed and written down") add_checkbox_paragraph(doc, shared == 'yes_not_written', "Yes – discussed, not written") add_checkbox_paragraph(doc, shared == 'not_yet', "Not yet") add_field_label(doc, "Notes from discussion:") add_field_value(doc, get_value(financial, 'sharing_notes')) add_sub_header(doc, "F. Overall Financial Wishes and Legacy Intentions") add_field_label(doc, "Summary of what matters most to you about how your financial affairs are handled:") add_field_value(doc, get_value(financial, 'overall_wishes')) add_field_label(doc, "Are there personal or sentimental items you want to designate for specific people?") has_items = is_true(financial.get('has_specific_items')) add_checkbox_paragraph(doc, has_items, "Yes (list below)") add_checkbox_paragraph(doc, not has_items, "Not yet decided") items = financial.get('specific_items', []) if items and has_items: add_field_label(doc, "Items / recipients:") for item in items: add_field_value(doc, f"– {item.get('item', 'Item')} → {item.get('recipient', 'Recipient')}") # ===== FUNERAL & MEMORIAL ===== doc.add_page_break() add_section_header(doc, "My Funeral & Memorial Plans") funeral = data.get('funeral_plans', {}) add_field_label(doc, "Type of service you prefer:") service_type = funeral.get('service_type', '') other_service = funeral.get('service_type_other') or '____________________' add_checkbox_paragraph(doc, service_type == 'funeral', "Funeral service") add_checkbox_paragraph(doc, service_type == 'memorial', "Memorial service") add_checkbox_paragraph(doc, service_type == 'celebration_of_life', "Celebration of Life") add_checkbox_paragraph(doc, service_type == 'other', f"Other: {other_service}") add_field_label(doc, "Body preference:") body_pref = funeral.get('body_preference', '') body_details = funeral.get('body_details') or '' burial_loc = body_details if body_pref == 'burial' and body_details else '____________________' crem_details = body_details if body_pref == 'cremation' and body_details else '____________________' add_checkbox_paragraph(doc, body_pref == 'burial', f"Burial (location: {burial_loc})") add_checkbox_paragraph(doc, body_pref == 'cremation', f"Cremation ({crem_details})") add_checkbox_paragraph(doc, body_pref == 'donation', "Donation to science") add_checkbox_paragraph(doc, body_pref == 'undecided', "Undecided") add_field_label(doc, "Summary of conversation:") add_field_value(doc, get_value(funeral, 'conversation_summary')) add_field_label(doc, "Special requests or details:") p = doc.add_paragraph() p.add_run("Preferred location: ").bold = True p.add_run(get_value(funeral, 'preferred_location')) p = doc.add_paragraph() p.add_run("Leader of service: ").bold = True p.add_run(get_value(funeral, 'service_leader')) p = doc.add_paragraph() p.add_run("Music/Readings: ").bold = True p.add_run(get_value(funeral, 'music_readings')) p = doc.add_paragraph() p.add_run("Appearance/Clothing: ").bold = True p.add_run(get_value(funeral, 'appearance_clothing')) p = doc.add_paragraph() p.add_run("Charities for donations: ").bold = True p.add_run(get_value(funeral, 'charity_donations')) add_field_label(doc, "Funeral cost planning:") cost = funeral.get('cost_planning', '') add_checkbox_paragraph(doc, cost == 'prepaid', "Pre-paid plan") add_checkbox_paragraph(doc, cost == 'family_aware', "Family aware of funding") add_checkbox_paragraph(doc, cost == 'needs_discussion', "Needs discussion") add_field_label(doc, "Additional notes:") add_field_value(doc, get_value(funeral, 'additional_notes')) # ===== VALUES & REFLECTIONS ===== add_section_header(doc, "My Values & Life Reflections") values = data.get('values_reflections', {}) add_field_label(doc, "What matters most to me about how I live and am remembered:") add_field_value(doc, get_value(values, 'what_matters_most')) add_field_label(doc, "What gives my life meaning and joy:") add_field_value(doc, get_value(values, 'meaning_and_joy')) add_field_label(doc, "What I hope my family and friends remember most:") add_field_value(doc, get_value(values, 'want_remembered_for')) # ===== NEXT STEPS ===== add_section_header(doc, "Next Steps & Resources") add_field_label(doc, "From today's conversation, the next steps we identified:") add_checkbox_paragraph(doc, True, "Update or create a Health Care Power of Attorney (POA) or Living Will") add_checkbox_paragraph(doc, True, "Provide copies of my Health Care POA and Living Will to my health care team") add_checkbox_paragraph(doc, True, "Complete or update Financial Power of Attorney / Will / Trust") add_checkbox_paragraph(doc, True, "Review and update beneficiaries on insurance, retirement, and bank accounts") add_checkbox_paragraph(doc, True, "Create or update a list of key financial information and tell my trusted person where it's stored") add_checkbox_paragraph(doc, True, "Talk with my loved ones about my wishes for health, finances, and funeral planning") add_checkbox_paragraph(doc, True, "Store all important documents safely in a clearly labeled folder or binder at home") add_checkbox_paragraph(doc, True, "Review all plans annually or after major life events") add_field_label(doc, "Facilitator Summary or Recommendations:") add_field_value(doc, get_value(data, 'facilitator_summary')) doc.save(output_path) return output_path # ============================================================================ # MAIN PROCESSING FUNCTION # ============================================================================ def process_audio(audio_file): """Main function to process audio and generate Word document.""" if audio_file is None: return None, "Please record or upload an audio file.", None api_key = os.environ.get("GEMINI_API_KEY") if not api_key: return None, "API key not configured. Please set GEMINI_API_KEY in Space secrets.", None try: # Analyze audio raw_response = analyze_audio(audio_file, api_key) # Parse response data = parse_json_response(raw_response) if not data: return None, "Failed to parse the AI response. Please try again.", None # Normalize data data = normalize_data(data) # Generate Word document participant_name = get_value(data, 'participant', 'name', default='Unknown') safe_name = re.sub(r'[^a-zA-Z0-9]', '_', participant_name) output_dir = tempfile.gettempdir() output_filename = os.path.join(output_dir, f"Planning_Summary_{safe_name}.docx") generate_docx(data, output_filename) # Return results json_output = json.dumps(data, indent=2) status = f"Successfully generated planning summary for {participant_name}" return output_filename, status, json_output except Exception as e: return None, f"Error: {str(e)}", None def on_recording_stop(audio_data): """ Called when recording stops. Automatically triggers processing. audio_data is a tuple of (sample_rate, audio_array) from microphone recording. """ if audio_data is None: return None, "No audio recorded.", None # Save the recorded audio to a temporary file import numpy as np from scipy.io import wavfile sample_rate, audio_array = audio_data # Create temporary wav file temp_dir = tempfile.gettempdir() temp_path = os.path.join(temp_dir, f"recording_{int(time.time())}.wav") # Ensure audio is in the right format if audio_array.dtype != np.int16: # Normalize and convert to int16 if audio_array.dtype == np.float32 or audio_array.dtype == np.float64: audio_array = (audio_array * 32767).astype(np.int16) else: audio_array = audio_array.astype(np.int16) wavfile.write(temp_path, sample_rate, audio_array) # Process the audio docx_file, status, json_data = process_audio(temp_path) return docx_file, status, json_data def process_uploaded_file(audio_file): """Process an uploaded audio file.""" if audio_file is None: return None, "Please upload an audio file.", None return process_audio(audio_file) # ============================================================================ # GRADIO INTERFACE # ============================================================================ # Custom theme with neutral colors custom_theme = gr.themes.Base( primary_hue=gr.themes.colors.slate, secondary_hue=gr.themes.colors.gray, neutral_hue=gr.themes.colors.gray, ).set( button_primary_background_fill="#1a1a1a", button_primary_background_fill_hover="#333333", button_primary_text_color="white", block_label_text_color="#374151", block_title_text_color="#111827", ) with gr.Blocks(title="Advance Care Planning") as demo: gr.Markdown(""" # Advance Care Planning Record or upload an audio conversation to generate a structured Word document summary report. """) with gr.Tabs(): with gr.TabItem("Record Audio"): gr.Markdown(""" **Instructions:** Click the microphone button to start recording. Click again to stop. The recording will be automatically analyzed when you stop. """) with gr.Row(): with gr.Column(scale=1): audio_recorder = gr.Audio( label="Recording", sources=["microphone"], type="numpy", interactive=True ) with gr.Column(scale=1): record_status = gr.Textbox(label="Status", interactive=False) record_docx_output = gr.File(label="Download Word Document") with gr.Accordion("View Extracted Data (JSON)", open=False): record_json_output = gr.Code(label="Extracted Data", language="json") # Auto-process when recording stops audio_recorder.stop_recording( fn=on_recording_stop, inputs=[audio_recorder], outputs=[record_docx_output, record_status, record_json_output] ) with gr.TabItem("Upload Audio"): with gr.Row(): with gr.Column(scale=1): audio_upload = gr.Audio( label="Upload Audio Recording", type="filepath", sources=["upload"] ) upload_btn = gr.Button("Analyze & Generate Word Doc", variant="primary") with gr.Column(scale=1): upload_status = gr.Textbox(label="Status", interactive=False) upload_docx_output = gr.File(label="Download Word Document") with gr.Accordion("View Extracted Data (JSON)", open=False): upload_json_output = gr.Code(label="Extracted Data", language="json") upload_btn.click( fn=process_uploaded_file, inputs=[audio_upload], outputs=[upload_docx_output, upload_status, upload_json_output] ) gr.Markdown(""" --- **Notes:** - Supported audio formats: MP3, WAV, M4A, and other common formats - The generated Word document is a summary document, not a legal document """) if __name__ == "__main__": demo.launch(theme=custom_theme)