stevafernandes commited on
Commit
196cb9f
·
verified ·
1 Parent(s): 288500b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +577 -387
app.py CHANGED
@@ -2,6 +2,17 @@
2
  Planning Summary Audio Analyzer - Hugging Face Spaces App
3
  Analyzes audio recordings of planning conversations and generates
4
  a structured Word document planning summary report using Google's Gemini API.
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  import os
@@ -9,8 +20,6 @@ import re
9
  import json
10
  import time
11
  import tempfile
12
- import gradio as gr
13
- import google.generativeai as genai
14
  from docx import Document
15
  from docx.shared import Inches, Pt, Twips
16
  from docx.enum.text import WD_ALIGN_PARAGRAPH
@@ -18,6 +27,19 @@ from docx.enum.style import WD_STYLE_TYPE
18
  from docx.oxml.ns import qn
19
  from docx.oxml import OxmlElement
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # ============================================================================
22
  # EXTRACTION PROMPT
23
  # ============================================================================
@@ -25,20 +47,29 @@ from docx.oxml import OxmlElement
25
  EXTRACTION_PROMPT = """
26
  You are analyzing a recorded conversation about advance care planning and end-of-life wishes.
27
  Listen to the ENTIRE audio carefully and extract ALL relevant information.
 
28
  CRITICAL INSTRUCTIONS FOR SINGLE-SELECT FIELDS:
29
  - You MUST select exactly ONE option for each single-select field
30
  - Use the EXACT string values specified (copy them exactly)
31
  - If the conversation implies something even indirectly, make your best inference
32
  - NEVER leave single-select fields as null - always pick the best match
 
 
 
 
 
 
 
33
  Return a JSON object with this EXACT structure:
34
  ```json
35
  {
36
  "participant": {
37
  "name": "First and last name if mentioned",
38
- "conversation_date": "MM/DD/YYYY format if mentioned",
39
- "facilitator": "Facilitator name and credentials",
40
- "location": "Location of conversation"
41
  },
 
42
  "health_care_wishes": {
43
  "primary_decision_maker": {
44
  "name": "Name",
@@ -47,14 +78,14 @@ Return a JSON object with this EXACT structure:
47
  "email": "email or null"
48
  },
49
  "backup_decision_maker": {
50
- "name": "Name",
51
- "relationship": "relationship",
52
  "phone": "phone or null"
53
  },
54
- "values_summary": "2-3 sentence summary of their values",
55
  "advance_care_status": "MUST BE ONE OF: has_current_documents | has_documents_needs_update | no_documents",
56
- "treatment_preference": "MUST BE ONE OF: comfort_care_only | full_treatment_if_recovery | unsure",
57
- "treatment_details": "Details about CPR, ventilation, ICU preferences",
58
  "additional_notes": "Other notes or null"
59
  },
60
  "financial_planning": {
@@ -65,11 +96,11 @@ Return a JSON object with this EXACT structure:
65
  "email": "email or null"
66
  },
67
  "financial_backup": {
68
- "name": "Name",
69
- "relationship": "relationship",
70
  "phone": "phone or null"
71
  },
72
- "financial_conversation_summary": "Summary of who handles finances",
73
  "documents_in_place": {
74
  "financial_poa": false,
75
  "will": false,
@@ -87,7 +118,7 @@ Return a JSON object with this EXACT structure:
87
  "other": "Other specific steps mentioned or null"
88
  },
89
  "beneficiary_status": "MUST BE ONE OF: all_current | need_to_update | unsure",
90
- "account_notes": "Notes about 401k, pension, retirement accounts",
91
  "beneficiary_conversation": "Summary of beneficiary discussion",
92
  "has_info_list": "MUST BE ONE OF: yes_shared | yes_not_shared | not_created",
93
  "info_location": "Where files/info are stored",
@@ -95,67 +126,115 @@ Return a JSON object with this EXACT structure:
95
  "shared_with_loved_ones": "MUST BE ONE OF: yes_written | yes_not_written | not_yet",
96
  "sharing_notes": "Notes about family discussions",
97
  "overall_wishes": "Summary of financial wishes",
98
- "has_specific_items": true,
99
  "specific_items": [
100
- {"item": "Fishing gear", "recipient": "Son Charlie"},
101
- {"item": "Guitar", "recipient": "Daughter Betsy"}
102
  ]
103
  },
104
  "funeral_plans": {
105
- "service_type": "MUST BE ONE OF: funeral | memorial | celebration_of_life | other",
106
  "service_type_other": "If other, describe",
107
- "body_preference": "MUST BE ONE OF: burial | cremation | donation | undecided",
108
  "body_details": "Location details like 'Ashes spread at favorite fishing lake'",
109
- "conversation_summary": "Summary of funeral wishes",
110
- "preferred_location": "Where service should be held",
111
- "service_leader": "Who should lead",
112
- "music_readings": "Music and reading preferences",
113
- "appearance_clothing": "Dress code preferences",
114
- "charity_donations": "Charity for donations",
115
- "cost_planning": "MUST BE ONE OF: prepaid | family_aware | needs_discussion",
116
  "additional_notes": "Notes about life insurance, costs, etc."
117
  },
118
  "values_reflections": {
119
- "what_matters_most": "How they want to be remembered",
120
- "meaning_and_joy": "What gives their life meaning",
121
- "want_remembered_for": "What they hope people remember"
 
 
 
 
 
 
 
 
 
 
 
 
122
  },
123
  "facilitator_summary": "Facilitator's closing summary and recommendations"
124
  }
125
  ```
 
126
  DECISION GUIDE FOR COMMON SCENARIOS:
 
 
 
 
 
 
127
  advance_care_status:
128
  - If they say they haven't done paperwork/documents yet -> "no_documents"
129
  - If they have old documents that need updating -> "has_documents_needs_update"
130
  - If they have current, up-to-date documents -> "has_current_documents"
 
131
  treatment_preference:
132
- - If they want treatment/CPR/ventilation IF there's hope of recovery -> "full_treatment_if_recovery"
133
- - If they only want comfort/palliative care, no machines -> "comfort_care_only"
 
 
 
134
  - If they're unsure or need more information -> "unsure"
 
135
  beneficiary_status:
 
 
 
 
 
136
  - If they're not sure who's listed or need to check -> "unsure"
137
- - If they know some need updating -> "need_to_update"
138
- - If everything is current -> "all_current"
139
  has_info_list:
140
- - If they have files/info but haven't shared location or it's disorganized -> "yes_not_shared"
141
  - If their trusted person knows where everything is -> "yes_shared"
142
  - If they haven't created any list -> "not_created"
 
143
  shared_with_loved_ones:
144
- - If they've talked but nothing written down -> "yes_not_written"
145
  - If they've discussed AND written it down -> "yes_written"
146
  - If they haven't discussed wishes yet -> "not_yet"
147
- service_type:
 
 
 
 
 
 
 
 
148
  - Celebration of life, casual gathering, party -> "celebration_of_life"
149
  - Traditional funeral -> "funeral"
150
  - Memorial service -> "memorial"
151
- body_preference:
152
  - Cremation, ashes spread somewhere -> "cremation"
153
  - Burial in cemetery/ground -> "burial"
154
  - Donate body to science -> "donation"
155
- cost_planning:
156
  - If they mention life insurance will cover it or family knows about funding -> "family_aware"
157
  - If they have a pre-paid funeral plan -> "prepaid"
158
  - If costs haven't been discussed -> "needs_discussion"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  Listen for these key topics:
160
  - Who would make healthcare decisions (usually spouse first, then adult child)
161
  - Who would handle finances (often same people)
@@ -164,6 +243,7 @@ Listen for these key topics:
164
  - Funeral/memorial preferences
165
  - Special items to give specific people
166
  - What matters most to them, their values
 
167
  Return ONLY valid JSON, no markdown formatting or explanation.
168
  """
169
 
@@ -173,20 +253,22 @@ Return ONLY valid JSON, no markdown formatting or explanation.
173
 
174
  def analyze_audio(audio_path: str, api_key: str) -> str:
175
  """Upload audio to Gemini and extract planning information."""
 
 
176
  genai.configure(api_key=api_key)
177
-
178
  audio_file = genai.upload_file(audio_path)
179
-
180
  # Wait for processing
181
  while audio_file.state.name == "PROCESSING":
182
  time.sleep(5)
183
  audio_file = genai.get_file(audio_file.name)
184
-
185
  if audio_file.state.name == "FAILED":
186
  raise ValueError(f"Audio processing failed: {audio_file.state.name}")
187
-
188
  model = genai.GenerativeModel('gemini-3-flash-preview')
189
-
190
  response = model.generate_content(
191
  [audio_file, EXTRACTION_PROMPT],
192
  generation_config=genai.GenerationConfig(
@@ -194,10 +276,10 @@ def analyze_audio(audio_path: str, api_key: str) -> str:
194
  max_output_tokens=8192
195
  )
196
  )
197
-
198
  # Cleanup uploaded file
199
  genai.delete_file(audio_file.name)
200
-
201
  return response.text
202
 
203
  # ============================================================================
@@ -208,7 +290,7 @@ def parse_json_response(response_text: str) -> dict | None:
208
  """Extract JSON from Gemini response."""
209
  cleaned = re.sub(r'```json\s*', '', response_text)
210
  cleaned = re.sub(r'```\s*', '', cleaned)
211
-
212
  json_match = re.search(r'\{[\s\S]*\}', cleaned)
213
  if json_match:
214
  try:
@@ -222,50 +304,54 @@ def normalize_value(value, valid_options, default=None):
222
  """Normalize a value to match one of the valid options."""
223
  if value is None:
224
  return default
225
-
226
  val_str = str(value).lower().strip()
227
  val_normalized = val_str.replace(' ', '_').replace('-', '_')
228
-
229
  # Direct match
230
  for opt in valid_options:
231
  if val_normalized == opt.lower():
232
  return opt
233
-
234
  # Fuzzy matching rules
235
  matching_rules = {
236
- 'no_documents': ['no_documents', 'none', 'not_completed', 'no documents', 'not yet'],
237
  'has_current_documents': ['has_current', 'current', 'up_to_date', 'have documents'],
238
  'has_documents_needs_update': ['needs_update', 'need_update', 'review', 'outdated'],
239
  'full_treatment_if_recovery': ['full_treatment', 'full treatment', 'aggressive', 'treatment if recovery'],
240
- 'comfort_care_only': ['comfort', 'comfort_care', 'palliative', 'no machines'],
 
241
  'unsure': ['unsure', 'not sure', 'uncertain', 'undecided', 'need more info'],
242
  'need_to_update': ['need_to_update', 'needs update', 'update', 'outdated'],
243
- 'all_current': ['all_current', 'current', 'up to date', 'all updated'],
244
  'yes_not_shared': ['yes_not_shared', 'yes but', 'have but', 'not shared', 'disorganized'],
245
  'yes_shared': ['yes_shared', 'yes shared', 'knows where', 'shared'],
246
- 'not_created': ['not_created', 'no', 'none', "haven't created", 'not yet'],
247
  'yes_not_written': ['yes_not_written', 'discussed not written', 'talked but', 'verbal', 'not written'],
248
  'yes_written': ['yes_written', 'written', 'documented', 'written down'],
249
- 'not_yet': ['not_yet', 'no', "haven't", 'not discussed'],
 
 
 
250
  'celebration_of_life': ['celebration', 'celebration_of_life', 'party', 'gathering', 'casual'],
251
  'funeral': ['funeral', 'traditional'],
252
  'memorial': ['memorial', 'memorial_service'],
253
  'other': ['other'],
 
254
  'cremation': ['cremation', 'cremate', 'ashes', 'cremated'],
255
  'burial': ['burial', 'bury', 'buried', 'cemetery', 'ground'],
256
  'donation': ['donation', 'donate', 'science', 'donate body'],
257
- 'undecided': ['undecided', 'unsure', 'not sure'],
258
  'family_aware': ['family_aware', 'family aware', 'life insurance', 'insurance', 'covered'],
259
  'prepaid': ['prepaid', 'pre-paid', 'pre paid', 'paid'],
260
- 'needs_discussion': ['needs_discussion', 'need to discuss', 'not discussed']
261
  }
262
-
263
  for opt in valid_options:
264
  if opt in matching_rules:
265
  for pattern in matching_rules[opt]:
266
  if pattern in val_str or pattern in val_normalized:
267
  return opt
268
-
269
  return default
270
 
271
 
@@ -273,7 +359,11 @@ def normalize_data(data: dict) -> dict:
273
  """Normalize all single-select fields in the extracted data."""
274
  if not data:
275
  return data
276
-
 
 
 
 
277
  # Health care wishes
278
  if 'health_care_wishes' in data:
279
  hcw = data['health_care_wishes']
@@ -284,10 +374,10 @@ def normalize_data(data: dict) -> dict:
284
  )
285
  hcw['treatment_preference'] = normalize_value(
286
  hcw.get('treatment_preference'),
287
- ['comfort_care_only', 'full_treatment_if_recovery', 'unsure'],
288
- 'full_treatment_if_recovery'
289
  )
290
-
291
  # Financial planning
292
  if 'financial_planning' in data:
293
  fp = data['financial_planning']
@@ -306,28 +396,94 @@ def normalize_data(data: dict) -> dict:
306
  ['yes_written', 'yes_not_written', 'not_yet'],
307
  'yes_not_written'
308
  )
309
-
 
 
 
 
 
310
  # Funeral plans
311
  if 'funeral_plans' in data:
312
  fun = data['funeral_plans']
313
  fun['service_type'] = normalize_value(
314
  fun.get('service_type'),
315
- ['funeral', 'memorial', 'celebration_of_life', 'other'],
316
- 'celebration_of_life'
317
  )
318
  fun['body_preference'] = normalize_value(
319
  fun.get('body_preference'),
320
- ['burial', 'cremation', 'donation', 'undecided'],
321
- 'cremation'
322
  )
323
  fun['cost_planning'] = normalize_value(
324
  fun.get('cost_planning'),
325
- ['prepaid', 'family_aware', 'needs_discussion'],
326
- 'family_aware'
327
  )
328
-
 
 
 
 
329
  return data
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  # ============================================================================
332
  # WORD DOCUMENT GENERATION
333
  # ============================================================================
@@ -429,19 +585,21 @@ def add_sub_header(doc, text):
429
  def generate_docx(data: dict, output_path: str) -> str:
430
  """Generate the planning summary Word document."""
431
  doc = Document()
432
-
433
  # Set default font
434
  style = doc.styles['Normal']
435
  style.font.name = 'Arial'
436
  style.font.size = Pt(10)
437
-
438
  # Set page margins (0.75 inch)
439
  for section in doc.sections:
440
  section.top_margin = Inches(0.6)
441
  section.bottom_margin = Inches(0.6)
442
  section.left_margin = Inches(0.75)
443
  section.right_margin = Inches(0.75)
444
-
 
 
445
  # ===== TITLE =====
446
  title = doc.add_paragraph()
447
  title.alignment = WD_ALIGN_PARAGRAPH.CENTER
@@ -449,26 +607,26 @@ def generate_docx(data: dict, output_path: str) -> str:
449
  title_run.font.name = 'Arial'
450
  title_run.font.size = Pt(16)
451
  title_run.bold = True
452
-
453
  participant = data.get('participant', {})
454
-
455
  # Participant info
456
  p = doc.add_paragraph()
457
  p.add_run("Participant Name: ").bold = True
458
  p.add_run(get_value(participant, 'name'))
459
-
460
  p = doc.add_paragraph()
461
  p.add_run("Date of Conversation: ").bold = True
462
  p.add_run(get_value(participant, 'conversation_date'))
463
-
464
  p = doc.add_paragraph()
465
  p.add_run("Facilitator: ").bold = True
466
  p.add_run(get_value(participant, 'facilitator'))
467
-
468
  p = doc.add_paragraph()
469
  p.add_run("Location: ").bold = True
470
  p.add_run(get_value(participant, 'location'))
471
-
472
  # Intro text
473
  intro = doc.add_paragraph()
474
  intro.paragraph_format.space_before = Pt(8)
@@ -479,231 +637,259 @@ def generate_docx(data: dict, output_path: str) -> str:
479
  )
480
  intro_run.font.size = Pt(9)
481
  intro_run.font.color.rgb = None # Use default color
482
-
483
  # ===== HEALTH & CARE WISHES =====
484
- add_section_header(doc, "My Health & Care Wishes")
485
-
486
- health = data.get('health_care_wishes', {})
487
-
488
- add_field_label(doc, "Who would you trust to make health decisions for you if you could not speak for yourself?")
489
-
490
- primary = health.get('primary_decision_maker', {})
491
- add_field_value(doc, f"Primary: {get_value(primary, 'name')} {get_value(primary, 'relationship')}")
492
- add_field_value(doc, f"Phone: {get_value(primary, 'phone')} | Email: {get_value(primary, 'email')}")
493
-
494
- backup = health.get('backup_decision_maker', {})
495
- add_field_value(doc, f"Back-up: {get_value(backup, 'name')} {get_value(backup, 'relationship')}")
496
- add_field_value(doc, f"Phone: {get_value(backup, 'phone')}")
497
-
498
- add_field_label(doc, "Summary of what you shared about your values and care priorities:")
499
- add_field_value(doc, get_value(health, 'values_summary'))
500
-
501
- add_field_label(doc, "Current advance-care planning status:")
502
- status = health.get('advance_care_status', '')
503
- add_checkbox_paragraph(doc, status == 'has_current_documents', "I have a health-care power of attorney or living will (up-to-date)")
504
- add_checkbox_paragraph(doc, status == 'has_documents_needs_update', "I have documents but need to review/update")
505
- add_checkbox_paragraph(doc, status == 'no_documents', "I have not yet completed these documents")
506
-
507
- add_field_label(doc, "Treatment preferences we discussed:")
508
- treatment = health.get('treatment_preference', '')
509
- add_checkbox_paragraph(doc, treatment == 'comfort_care_only', "Comfort care only (no machines or resuscitation if dying)")
510
- add_checkbox_paragraph(doc, treatment == 'full_treatment_if_recovery', "Full medical treatment if recovery possible")
511
- add_checkbox_paragraph(doc, treatment == 'unsure', "Unsure / would like more information")
512
-
513
- add_field_label(doc, "Additional details related to treatment preferences that were discussed:")
514
- add_field_value(doc, get_value(health, 'treatment_details'))
515
-
516
- add_field_label(doc, "Notes from conversation:")
517
- add_field_value(doc, get_value(health, 'additional_notes'))
518
-
 
 
519
  # ===== FINANCIAL PLANNING =====
520
- doc.add_page_break()
521
- add_section_header(doc, "My Financial Planning & Legacy")
522
-
523
- financial = data.get('financial_planning', {})
524
-
525
- add_sub_header(doc, "A. Trusted Person(s)")
526
- add_field_label(doc, "Who do you trust to manage your finances if you are unable?")
527
-
528
- fin_primary = financial.get('financial_primary', {})
529
- add_field_value(doc, f"Primary: {get_value(fin_primary, 'name')} {get_value(fin_primary, 'relationship')}")
530
- add_field_value(doc, f"Phone: {get_value(fin_primary, 'phone')} | Email: {get_value(fin_primary, 'email')}")
531
-
532
- fin_backup = financial.get('financial_backup', {})
533
- add_field_value(doc, f"Back-up: {get_value(fin_backup, 'name')} {get_value(fin_backup, 'relationship')}")
534
- add_field_value(doc, f"Phone: {get_value(fin_backup, 'phone')}")
535
-
536
- add_field_label(doc, "Conversation summary:")
537
- add_field_value(doc, get_value(financial, 'financial_conversation_summary'))
538
-
539
- add_sub_header(doc, "B. Legal and Financial Readiness")
540
- add_field_label(doc, "Documents currently in place (check all that apply):")
541
-
542
- docs = financial.get('documents_in_place', {})
543
- add_checkbox_paragraph(doc, is_true(docs.get('financial_poa')), "Financial Power of Attorney (POA)")
544
- add_checkbox_paragraph(doc, is_true(docs.get('will')), "Will")
545
- add_checkbox_paragraph(doc, is_true(docs.get('living_trust')), "Living Trust")
546
- add_checkbox_paragraph(doc, is_true(docs.get('tod_designations')), "Transfer on Death (TOD) designations")
547
- add_checkbox_paragraph(doc, is_true(docs.get('joint_ownership')), "Joint ownership of key accounts")
548
- add_checkbox_paragraph(doc, is_true(docs.get('none')), "None completed yet")
549
-
550
- add_field_label(doc, "Details from conversation:")
551
- add_field_value(doc, get_value(financial, 'legal_details'))
552
-
553
- add_field_label(doc, "Next steps (as discussed):")
554
- next_steps = financial.get('next_steps', {})
555
- add_checkbox_paragraph(doc, is_true(next_steps.get('review_update_poa_will')), "Review or update existing POA or Will")
556
- add_checkbox_paragraph(doc, is_true(next_steps.get('identify_alternate')), "Identify alternate decision-maker")
557
- add_checkbox_paragraph(doc, is_true(next_steps.get('contact_attorney')), "Contact attorney or legal aid for document preparation")
558
- add_checkbox_paragraph(doc, is_true(next_steps.get('seek_trust_advice')), "Seek advice on creating or updating a Trust")
559
- other_steps = next_steps.get('other')
560
- if other_steps and other_steps not in ('null', None, 'Not discussed', ''):
561
- add_checkbox_paragraph(doc, True, f"Other: {other_steps}")
562
-
563
- add_sub_header(doc, "C. Beneficiaries and Account Management")
564
- add_field_label(doc, "Status of major accounts and beneficiaries:")
565
-
566
- ben_status = financial.get('beneficiary_status', '')
567
- add_checkbox_paragraph(doc, ben_status == 'all_current', "All beneficiaries current and reflect my wishes")
568
- add_checkbox_paragraph(doc, ben_status == 'need_to_update', "Need to review or update some")
569
- add_checkbox_paragraph(doc, ben_status == 'unsure', "Unsure / need help locating information")
570
-
571
- add_field_label(doc, "Notes about specific accounts (bank, insurance, retirement):")
572
- add_field_value(doc, get_value(financial, 'account_notes'))
573
-
574
- add_field_label(doc, "Conversation summary:")
575
- add_field_value(doc, get_value(financial, 'beneficiary_conversation'))
576
-
577
- add_sub_header(doc, "D. Organizing Financial Information")
578
- add_field_label(doc, "Do you have a list of key information (accounts, passwords, insurance details)?")
579
-
580
- info_status = financial.get('has_info_list', '')
581
- add_checkbox_paragraph(doc, info_status == 'yes_shared', "Yes – my trusted person knows where it is")
582
- add_checkbox_paragraph(doc, info_status == 'yes_not_shared', "Yes but not shared or outdated")
583
- add_checkbox_paragraph(doc, info_status == 'not_created', "Not yet created")
584
-
585
- add_field_label(doc, "Where this information can be found:")
586
- add_field_value(doc, get_value(financial, 'info_location'))
587
-
588
- add_field_label(doc, "Additional organization ideas shared in conversation:")
589
- add_field_value(doc, get_value(financial, 'organization_ideas'))
590
-
591
- add_sub_header(doc, "E. Talking with Loved Ones")
592
- add_field_label(doc, "Have you shared your financial wishes with loved ones or trusted helpers?")
593
-
594
- shared = financial.get('shared_with_loved_ones', '')
595
- add_checkbox_paragraph(doc, shared == 'yes_written', "Yes – discussed and written down")
596
- add_checkbox_paragraph(doc, shared == 'yes_not_written', "Yes discussed, not written")
597
- add_checkbox_paragraph(doc, shared == 'not_yet', "Not yet")
598
-
599
- add_field_label(doc, "Notes from discussion:")
600
- add_field_value(doc, get_value(financial, 'sharing_notes'))
601
-
602
- add_sub_header(doc, "F. Overall Financial Wishes and Legacy Intentions")
603
- add_field_label(doc, "Summary of what matters most to you about how your financial affairs are handled:")
604
- add_field_value(doc, get_value(financial, 'overall_wishes'))
605
-
606
- add_field_label(doc, "Are there personal or sentimental items you want to designate for specific people?")
607
- has_items = is_true(financial.get('has_specific_items'))
608
- add_checkbox_paragraph(doc, has_items, "Yes (list below)")
609
- add_checkbox_paragraph(doc, not has_items, "Not yet decided")
610
-
611
- items = financial.get('specific_items', [])
612
- if items and has_items:
613
- add_field_label(doc, "Items / recipients:")
614
- for item in items:
615
- add_field_value(doc, f" {item.get('item', 'Item')} → {item.get('recipient', 'Recipient')}")
616
-
 
 
617
  # ===== FUNERAL & MEMORIAL =====
618
- doc.add_page_break()
619
- add_section_header(doc, "My Funeral & Memorial Plans")
620
-
621
- funeral = data.get('funeral_plans', {})
622
-
623
- add_field_label(doc, "Type of service you prefer:")
624
- service_type = funeral.get('service_type', '')
625
- other_service = funeral.get('service_type_other') or '____________________'
626
- add_checkbox_paragraph(doc, service_type == 'funeral', "Funeral service")
627
- add_checkbox_paragraph(doc, service_type == 'memorial', "Memorial service")
628
- add_checkbox_paragraph(doc, service_type == 'celebration_of_life', "Celebration of Life")
629
- add_checkbox_paragraph(doc, service_type == 'other', f"Other: {other_service}")
630
-
631
- add_field_label(doc, "Body preference:")
632
- body_pref = funeral.get('body_preference', '')
633
- body_details = funeral.get('body_details') or ''
634
-
635
- burial_loc = body_details if body_pref == 'burial' and body_details else '____________________'
636
- crem_details = body_details if body_pref == 'cremation' and body_details else '____________________'
637
-
638
- add_checkbox_paragraph(doc, body_pref == 'burial', f"Burial (location: {burial_loc})")
639
- add_checkbox_paragraph(doc, body_pref == 'cremation', f"Cremation ({crem_details})")
640
- add_checkbox_paragraph(doc, body_pref == 'donation', "Donation to science")
641
- add_checkbox_paragraph(doc, body_pref == 'undecided', "Undecided")
642
-
643
- add_field_label(doc, "Summary of conversation:")
644
- add_field_value(doc, get_value(funeral, 'conversation_summary'))
645
-
646
- add_field_label(doc, "Special requests or details:")
647
-
648
- p = doc.add_paragraph()
649
- p.add_run("Preferred location: ").bold = True
650
- p.add_run(get_value(funeral, 'preferred_location'))
651
-
652
- p = doc.add_paragraph()
653
- p.add_run("Leader of service: ").bold = True
654
- p.add_run(get_value(funeral, 'service_leader'))
655
-
656
- p = doc.add_paragraph()
657
- p.add_run("Music/Readings: ").bold = True
658
- p.add_run(get_value(funeral, 'music_readings'))
659
-
660
- p = doc.add_paragraph()
661
- p.add_run("Appearance/Clothing: ").bold = True
662
- p.add_run(get_value(funeral, 'appearance_clothing'))
663
-
664
- p = doc.add_paragraph()
665
- p.add_run("Charities for donations: ").bold = True
666
- p.add_run(get_value(funeral, 'charity_donations'))
667
-
668
- add_field_label(doc, "Funeral cost planning:")
669
- cost = funeral.get('cost_planning', '')
670
- add_checkbox_paragraph(doc, cost == 'prepaid', "Pre-paid plan")
671
- add_checkbox_paragraph(doc, cost == 'family_aware', "Family aware of funding")
672
- add_checkbox_paragraph(doc, cost == 'needs_discussion', "Needs discussion")
673
-
674
- add_field_label(doc, "Additional notes:")
675
- add_field_value(doc, get_value(funeral, 'additional_notes'))
676
-
 
 
 
 
 
 
677
  # ===== VALUES & REFLECTIONS =====
678
  add_section_header(doc, "My Values & Life Reflections")
679
-
680
  values = data.get('values_reflections', {})
681
-
682
  add_field_label(doc, "What matters most to me about how I live and am remembered:")
683
  add_field_value(doc, get_value(values, 'what_matters_most'))
684
-
685
  add_field_label(doc, "What gives my life meaning and joy:")
686
  add_field_value(doc, get_value(values, 'meaning_and_joy'))
687
-
688
  add_field_label(doc, "What I hope my family and friends remember most:")
689
  add_field_value(doc, get_value(values, 'want_remembered_for'))
690
-
691
- # ===== NEXT STEPS =====
692
  add_section_header(doc, "Next Steps & Resources")
693
  add_field_label(doc, "From today's conversation, the next steps we identified:")
694
-
695
- add_checkbox_paragraph(doc, True, "Update or create a Health Care Power of Attorney (POA) or Living Will")
696
- add_checkbox_paragraph(doc, True, "Provide copies of my Health Care POA and Living Will to my health care team")
697
- add_checkbox_paragraph(doc, True, "Complete or update Financial Power of Attorney / Will / Trust")
698
- add_checkbox_paragraph(doc, True, "Review and update beneficiaries on insurance, retirement, and bank accounts")
699
- add_checkbox_paragraph(doc, True, "Create or update a list of key financial information and tell my trusted person where it's stored")
700
- add_checkbox_paragraph(doc, True, "Talk with my loved ones about my wishes for health, finances, and funeral planning")
701
- add_checkbox_paragraph(doc, True, "Store all important documents safely in a clearly labeled folder or binder at home")
702
- add_checkbox_paragraph(doc, True, "Review all plans annually or after major life events")
703
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  add_field_label(doc, "Facilitator Summary or Recommendations:")
705
  add_field_value(doc, get_value(data, 'facilitator_summary'))
706
-
707
  doc.save(output_path)
708
  return output_path
709
 
@@ -715,38 +901,38 @@ def process_audio(audio_file):
715
  """Main function to process audio and generate Word document."""
716
  if audio_file is None:
717
  return None, "Please record or upload an audio file.", None
718
-
719
  api_key = os.environ.get("GEMINI_API_KEY")
720
  if not api_key:
721
  return None, "API key not configured. Please set GEMINI_API_KEY in Space secrets.", None
722
-
723
  try:
724
  # Analyze audio
725
  raw_response = analyze_audio(audio_file, api_key)
726
-
727
  # Parse response
728
  data = parse_json_response(raw_response)
729
  if not data:
730
  return None, "Failed to parse the AI response. Please try again.", None
731
-
732
  # Normalize data
733
  data = normalize_data(data)
734
-
735
  # Generate Word document
736
  participant_name = get_value(data, 'participant', 'name', default='Unknown')
737
  safe_name = re.sub(r'[^a-zA-Z0-9]', '_', participant_name)
738
-
739
  output_dir = tempfile.gettempdir()
740
  output_filename = os.path.join(output_dir, f"Planning_Summary_{safe_name}.docx")
741
-
742
  generate_docx(data, output_filename)
743
-
744
  # Return results
745
  json_output = json.dumps(data, indent=2)
746
  status = f"Successfully generated planning summary for {participant_name}"
747
-
748
  return output_filename, status, json_output
749
-
750
  except Exception as e:
751
  return None, f"Error: {str(e)}", None
752
 
@@ -758,17 +944,17 @@ def on_recording_stop(audio_data):
758
  """
759
  if audio_data is None:
760
  return None, "No audio recorded.", None
761
-
762
  # Save the recorded audio to a temporary file
763
  import numpy as np
764
  from scipy.io import wavfile
765
-
766
  sample_rate, audio_array = audio_data
767
-
768
  # Create temporary wav file
769
  temp_dir = tempfile.gettempdir()
770
  temp_path = os.path.join(temp_dir, f"recording_{int(time.time())}.wav")
771
-
772
  # Ensure audio is in the right format
773
  if audio_array.dtype != np.int16:
774
  # Normalize and convert to int16
@@ -776,12 +962,12 @@ def on_recording_stop(audio_data):
776
  audio_array = (audio_array * 32767).astype(np.int16)
777
  else:
778
  audio_array = audio_array.astype(np.int16)
779
-
780
  wavfile.write(temp_path, sample_rate, audio_array)
781
-
782
  # Process the audio
783
  docx_file, status, json_data = process_audio(temp_path)
784
-
785
  return docx_file, status, json_data
786
 
787
 
@@ -789,93 +975,97 @@ def process_uploaded_file(audio_file):
789
  """Process an uploaded audio file."""
790
  if audio_file is None:
791
  return None, "Please upload an audio file.", None
792
-
793
  return process_audio(audio_file)
794
 
795
  # ============================================================================
796
  # GRADIO INTERFACE
797
  # ============================================================================
798
 
799
- # Custom theme with neutral colors
800
- custom_theme = gr.themes.Base(
801
- primary_hue=gr.themes.colors.slate,
802
- secondary_hue=gr.themes.colors.gray,
803
- neutral_hue=gr.themes.colors.gray,
804
- ).set(
805
- button_primary_background_fill="#1a1a1a",
806
- button_primary_background_fill_hover="#333333",
807
- button_primary_text_color="white",
808
- block_label_text_color="#374151",
809
- block_title_text_color="#111827",
810
- )
811
-
812
- with gr.Blocks(title="Advance Care Planning") as demo:
813
- gr.Markdown("""
814
- # Advance Care Planning
815
-
816
- Record or upload an audio conversation to generate a structured Word document summary report.
817
- """)
818
-
819
- with gr.Tabs():
820
- with gr.TabItem("Record Audio"):
821
- gr.Markdown("""
822
- **Instructions:** Click the microphone button to start recording. Click again to stop.
823
- The recording will be automatically analyzed when you stop.
824
- """)
825
-
826
- with gr.Row():
827
- with gr.Column(scale=1):
828
- audio_recorder = gr.Audio(
829
- label="Recording",
830
- sources=["microphone"],
831
- type="numpy",
832
- interactive=True
833
- )
834
-
835
- with gr.Column(scale=1):
836
- record_status = gr.Textbox(label="Status", interactive=False)
837
- record_docx_output = gr.File(label="Download Word Document")
838
-
839
- with gr.Accordion("View Extracted Data (JSON)", open=False):
840
- record_json_output = gr.Code(label="Extracted Data", language="json")
841
-
842
- # Auto-process when recording stops
843
- audio_recorder.stop_recording(
844
- fn=on_recording_stop,
845
- inputs=[audio_recorder],
846
- outputs=[record_docx_output, record_status, record_json_output]
847
- )
848
-
849
- with gr.TabItem("Upload Audio"):
850
- with gr.Row():
851
- with gr.Column(scale=1):
852
- audio_upload = gr.Audio(
853
- label="Upload Audio Recording",
854
- type="filepath",
855
- sources=["upload"]
856
- )
857
-
858
- upload_btn = gr.Button("Analyze & Generate Word Doc", variant="primary")
859
-
860
- with gr.Column(scale=1):
861
- upload_status = gr.Textbox(label="Status", interactive=False)
862
- upload_docx_output = gr.File(label="Download Word Document")
863
-
864
- with gr.Accordion("View Extracted Data (JSON)", open=False):
865
- upload_json_output = gr.Code(label="Extracted Data", language="json")
866
-
867
- upload_btn.click(
868
- fn=process_uploaded_file,
869
- inputs=[audio_upload],
870
- outputs=[upload_docx_output, upload_status, upload_json_output]
871
- )
872
-
873
- gr.Markdown("""
874
- ---
875
- **Notes:**
876
- - Supported audio formats: MP3, WAV, M4A, and other common formats
877
- - The generated Word document is a summary document, not a legal document
878
- """)
 
879
 
880
  if __name__ == "__main__":
881
- demo.launch(theme=custom_theme)
 
 
 
 
2
  Planning Summary Audio Analyzer - Hugging Face Spaces App
3
  Analyzes audio recordings of planning conversations and generates
4
  a structured Word document planning summary report using Google's Gemini API.
5
+
6
+ CHANGELOG (corrections applied):
7
+ 1. Treatment preference: added "conditional_comfort_care" option for nuanced cases
8
+ 2. Beneficiary status: improved prompt guidance to distinguish account access from
9
+ formal beneficiary designation
10
+ 3. Values vs. care preferences: clarified prompt so medical decision criteria are not
11
+ conflated with life meaning/joy
12
+ 4. Personal items: added "no_specific_items" option (deliberate choice vs. indecision)
13
+ 5. Name spelling: prompt now flags uncertain proper noun spellings with [verify spelling]
14
+ 6. Next Steps section: driven by extracted data and topics discussed, not hardcoded
15
+ 7. Prompt includes "topics_discussed" field so the report only covers relevant sections
16
  """
17
 
18
  import os
 
20
  import json
21
  import time
22
  import tempfile
 
 
23
  from docx import Document
24
  from docx.shared import Inches, Pt, Twips
25
  from docx.enum.text import WD_ALIGN_PARAGRAPH
 
27
  from docx.oxml.ns import qn
28
  from docx.oxml import OxmlElement
29
 
30
+ # Defer heavy/optional imports so core logic is testable without them
31
+ try:
32
+ import gradio as gr
33
+ HAS_GRADIO = True
34
+ except ImportError:
35
+ HAS_GRADIO = False
36
+
37
+ try:
38
+ import google.generativeai as genai
39
+ HAS_GENAI = True
40
+ except ImportError:
41
+ HAS_GENAI = False
42
+
43
  # ============================================================================
44
  # EXTRACTION PROMPT
45
  # ============================================================================
 
47
  EXTRACTION_PROMPT = """
48
  You are analyzing a recorded conversation about advance care planning and end-of-life wishes.
49
  Listen to the ENTIRE audio carefully and extract ALL relevant information.
50
+
51
  CRITICAL INSTRUCTIONS FOR SINGLE-SELECT FIELDS:
52
  - You MUST select exactly ONE option for each single-select field
53
  - Use the EXACT string values specified (copy them exactly)
54
  - If the conversation implies something even indirectly, make your best inference
55
  - NEVER leave single-select fields as null - always pick the best match
56
+
57
+ IMPORTANT RULES FOR PROPER NOUNS:
58
+ - If a last name is spelled out letter by letter, use that exact spelling.
59
+ - If a last name is only spoken (not spelled), transcribe it phonetically and append
60
+ [verify spelling] after it. Example: "Potoff [verify spelling]"
61
+ - First names that are spelled out should use the spelled version.
62
+
63
  Return a JSON object with this EXACT structure:
64
  ```json
65
  {
66
  "participant": {
67
  "name": "First and last name if mentioned",
68
+ "conversation_date": "MM/DD/YYYY format if mentioned, or null",
69
+ "facilitator": "Facilitator name and credentials, or 'Not discussed'",
70
+ "location": "Location of conversation, or 'Not discussed'"
71
  },
72
+ "topics_discussed": ["health", "financial", "funeral"],
73
  "health_care_wishes": {
74
  "primary_decision_maker": {
75
  "name": "Name",
 
78
  "email": "email or null"
79
  },
80
  "backup_decision_maker": {
81
+ "name": "Name or null",
82
+ "relationship": "relationship or null",
83
  "phone": "phone or null"
84
  },
85
+ "values_summary": "2-3 sentence summary of their values and care priorities",
86
  "advance_care_status": "MUST BE ONE OF: has_current_documents | has_documents_needs_update | no_documents",
87
+ "treatment_preference": "MUST BE ONE OF: comfort_care_only | full_treatment_if_recovery | conditional_comfort_care | unsure",
88
+ "treatment_details": "Details about specific conditions, CPR, ventilation, ICU preferences",
89
  "additional_notes": "Other notes or null"
90
  },
91
  "financial_planning": {
 
96
  "email": "email or null"
97
  },
98
  "financial_backup": {
99
+ "name": "Name or null",
100
+ "relationship": "relationship or null",
101
  "phone": "phone or null"
102
  },
103
+ "financial_conversation_summary": "Summary of who handles finances and how",
104
  "documents_in_place": {
105
  "financial_poa": false,
106
  "will": false,
 
118
  "other": "Other specific steps mentioned or null"
119
  },
120
  "beneficiary_status": "MUST BE ONE OF: all_current | need_to_update | unsure",
121
+ "account_notes": "Notes about specific accounts, 401k, pension, retirement",
122
  "beneficiary_conversation": "Summary of beneficiary discussion",
123
  "has_info_list": "MUST BE ONE OF: yes_shared | yes_not_shared | not_created",
124
  "info_location": "Where files/info are stored",
 
126
  "shared_with_loved_ones": "MUST BE ONE OF: yes_written | yes_not_written | not_yet",
127
  "sharing_notes": "Notes about family discussions",
128
  "overall_wishes": "Summary of financial wishes",
129
+ "specific_items_status": "MUST BE ONE OF: has_specific_items | no_specific_items | not_yet_decided",
130
  "specific_items": [
131
+ {"item": "Description of item", "recipient": "Intended recipient"}
 
132
  ]
133
  },
134
  "funeral_plans": {
135
+ "service_type": "MUST BE ONE OF: funeral | memorial | celebration_of_life | other | not_discussed",
136
  "service_type_other": "If other, describe",
137
+ "body_preference": "MUST BE ONE OF: burial | cremation | donation | undecided | not_discussed",
138
  "body_details": "Location details like 'Ashes spread at favorite fishing lake'",
139
+ "conversation_summary": "Summary of funeral wishes discussion",
140
+ "preferred_location": "Where service should be held or null",
141
+ "service_leader": "Who should lead or null",
142
+ "music_readings": "Music and reading preferences or null",
143
+ "appearance_clothing": "Dress code preferences or null",
144
+ "charity_donations": "Charity for donations or null",
145
+ "cost_planning": "MUST BE ONE OF: prepaid | family_aware | needs_discussion | not_discussed",
146
  "additional_notes": "Notes about life insurance, costs, etc."
147
  },
148
  "values_reflections": {
149
+ "what_matters_most": "How they want to live and be remembered, based on legacy statements",
150
+ "meaning_and_joy": "Hobbies, relationships, activities, and sources of happiness mentioned OUTSIDE of medical decision-making context",
151
+ "want_remembered_for": "What they explicitly said they hope people remember about them"
152
+ },
153
+ "recommended_next_steps": {
154
+ "create_healthcare_poa": false,
155
+ "provide_poa_to_healthcare_team": false,
156
+ "complete_financial_poa_will_trust": false,
157
+ "review_update_beneficiaries": false,
158
+ "create_financial_info_list": false,
159
+ "discuss_wishes_with_loved_ones": false,
160
+ "store_documents_safely": false,
161
+ "review_plans_annually": false,
162
+ "explore_funeral_preplanning": false,
163
+ "other_steps": ["any other specific steps identified in conversation"]
164
  },
165
  "facilitator_summary": "Facilitator's closing summary and recommendations"
166
  }
167
  ```
168
+
169
  DECISION GUIDE FOR COMMON SCENARIOS:
170
+
171
+ topics_discussed:
172
+ - Listen for which topics the participant chose to focus on
173
+ - Only include "health", "financial", and/or "funeral" if they were actually discussed
174
+ - If the participant said they only want to discuss health and financial, do NOT include "funeral"
175
+
176
  advance_care_status:
177
  - If they say they haven't done paperwork/documents yet -> "no_documents"
178
  - If they have old documents that need updating -> "has_documents_needs_update"
179
  - If they have current, up-to-date documents -> "has_current_documents"
180
+
181
  treatment_preference:
182
+ - If they UNCONDITIONALLY want only comfort/palliative care, no machines ever -> "comfort_care_only"
183
+ - If they want full treatment/CPR/ventilation IF there's hope of meaningful recovery -> "full_treatment_if_recovery"
184
+ - If their preference DEPENDS ON CONDITIONS such as cognitive function, prognosis,
185
+ or quality of life (e.g. "treat me if I can still think clearly, but let me go
186
+ if I'm cognitively impaired") -> "conditional_comfort_care"
187
  - If they're unsure or need more information -> "unsure"
188
+
189
  beneficiary_status:
190
+ - IMPORTANT: "all_current" means the participant explicitly confirmed that formal
191
+ beneficiary designations (not just account access) are filed and up to date
192
+ - If they say their trusted person "has access" or "knows about" the accounts but
193
+ did NOT explicitly confirm legal beneficiary designations are current -> "unsure"
194
+ - If they know some designations need updating -> "need_to_update"
195
  - If they're not sure who's listed or need to check -> "unsure"
196
+
 
197
  has_info_list:
198
+ - If they have files/info but haven't shared the location or it's disorganized -> "yes_not_shared"
199
  - If their trusted person knows where everything is -> "yes_shared"
200
  - If they haven't created any list -> "not_created"
201
+
202
  shared_with_loved_ones:
203
+ - If they've talked but nothing is written down -> "yes_not_written"
204
  - If they've discussed AND written it down -> "yes_written"
205
  - If they haven't discussed wishes yet -> "not_yet"
206
+
207
+ specific_items_status:
208
+ - If they named specific items for specific people -> "has_specific_items"
209
+ - If they explicitly said no specific designations are needed (e.g. "everything goes
210
+ to my spouse" or "nothing specific needs to go anywhere specific") -> "no_specific_items"
211
+ - If they haven't thought about it yet or are undecided -> "not_yet_decided"
212
+
213
+ service_type / body_preference / cost_planning:
214
+ - If funeral planning was NOT discussed at all, use "not_discussed" for these fields
215
  - Celebration of life, casual gathering, party -> "celebration_of_life"
216
  - Traditional funeral -> "funeral"
217
  - Memorial service -> "memorial"
 
218
  - Cremation, ashes spread somewhere -> "cremation"
219
  - Burial in cemetery/ground -> "burial"
220
  - Donate body to science -> "donation"
 
221
  - If they mention life insurance will cover it or family knows about funding -> "family_aware"
222
  - If they have a pre-paid funeral plan -> "prepaid"
223
  - If costs haven't been discussed -> "needs_discussion"
224
+
225
+ values_reflections:
226
+ - "meaning_and_joy": ONLY include hobbies, relationships, passions, and activities
227
+ that bring happiness. Do NOT include medical decision criteria like cognitive
228
+ function preferences here. Those belong in treatment_details.
229
+ - "want_remembered_for": Use the participant's own words about how they want to be remembered.
230
+ - "what_matters_most": Summarize their overall philosophy about living and legacy.
231
+
232
+ recommended_next_steps:
233
+ - Set each to true ONLY if it is relevant based on what was discussed
234
+ - For example, if funeral planning was not discussed, do not set explore_funeral_preplanning to true
235
+ - If documents already exist and are current, do not set create_healthcare_poa to true
236
+ - Base these on gaps identified during the conversation
237
+
238
  Listen for these key topics:
239
  - Who would make healthcare decisions (usually spouse first, then adult child)
240
  - Who would handle finances (often same people)
 
243
  - Funeral/memorial preferences
244
  - Special items to give specific people
245
  - What matters most to them, their values
246
+
247
  Return ONLY valid JSON, no markdown formatting or explanation.
248
  """
249
 
 
253
 
254
  def analyze_audio(audio_path: str, api_key: str) -> str:
255
  """Upload audio to Gemini and extract planning information."""
256
+ if not HAS_GENAI:
257
+ raise RuntimeError("google-generativeai package is required for audio analysis")
258
  genai.configure(api_key=api_key)
259
+
260
  audio_file = genai.upload_file(audio_path)
261
+
262
  # Wait for processing
263
  while audio_file.state.name == "PROCESSING":
264
  time.sleep(5)
265
  audio_file = genai.get_file(audio_file.name)
266
+
267
  if audio_file.state.name == "FAILED":
268
  raise ValueError(f"Audio processing failed: {audio_file.state.name}")
269
+
270
  model = genai.GenerativeModel('gemini-3-flash-preview')
271
+
272
  response = model.generate_content(
273
  [audio_file, EXTRACTION_PROMPT],
274
  generation_config=genai.GenerationConfig(
 
276
  max_output_tokens=8192
277
  )
278
  )
279
+
280
  # Cleanup uploaded file
281
  genai.delete_file(audio_file.name)
282
+
283
  return response.text
284
 
285
  # ============================================================================
 
290
  """Extract JSON from Gemini response."""
291
  cleaned = re.sub(r'```json\s*', '', response_text)
292
  cleaned = re.sub(r'```\s*', '', cleaned)
293
+
294
  json_match = re.search(r'\{[\s\S]*\}', cleaned)
295
  if json_match:
296
  try:
 
304
  """Normalize a value to match one of the valid options."""
305
  if value is None:
306
  return default
307
+
308
  val_str = str(value).lower().strip()
309
  val_normalized = val_str.replace(' ', '_').replace('-', '_')
310
+
311
  # Direct match
312
  for opt in valid_options:
313
  if val_normalized == opt.lower():
314
  return opt
315
+
316
  # Fuzzy matching rules
317
  matching_rules = {
318
+ 'no_documents': ['no_documents', 'none', 'not_completed', 'no documents', 'not yet completed'],
319
  'has_current_documents': ['has_current', 'current', 'up_to_date', 'have documents'],
320
  'has_documents_needs_update': ['needs_update', 'need_update', 'review', 'outdated'],
321
  'full_treatment_if_recovery': ['full_treatment', 'full treatment', 'aggressive', 'treatment if recovery'],
322
+ 'conditional_comfort_care': ['conditional', 'depends on', 'conditional_comfort', 'if cognitive', 'condition based'],
323
+ 'comfort_care_only': ['comfort_care_only', 'comfort care only', 'palliative only', 'no machines ever', 'only comfort'],
324
  'unsure': ['unsure', 'not sure', 'uncertain', 'undecided', 'need more info'],
325
  'need_to_update': ['need_to_update', 'needs update', 'update', 'outdated'],
326
+ 'all_current': ['all_current', 'all current', 'confirmed current', 'designations current'],
327
  'yes_not_shared': ['yes_not_shared', 'yes but', 'have but', 'not shared', 'disorganized'],
328
  'yes_shared': ['yes_shared', 'yes shared', 'knows where', 'shared'],
329
+ 'not_created': ['not_created', 'no list', 'none created', "haven't created", 'not yet created'],
330
  'yes_not_written': ['yes_not_written', 'discussed not written', 'talked but', 'verbal', 'not written'],
331
  'yes_written': ['yes_written', 'written', 'documented', 'written down'],
332
+ 'not_yet': ['not_yet', "haven't discussed", 'not discussed yet'],
333
+ 'has_specific_items': ['has_specific', 'yes_specific', 'has items', 'specific items'],
334
+ 'no_specific_items': ['no_specific', 'no specific', 'everything to', 'nothing specific', 'no designations'],
335
+ 'not_yet_decided': ['not_yet_decided', 'not decided', 'undecided', "haven't thought"],
336
  'celebration_of_life': ['celebration', 'celebration_of_life', 'party', 'gathering', 'casual'],
337
  'funeral': ['funeral', 'traditional'],
338
  'memorial': ['memorial', 'memorial_service'],
339
  'other': ['other'],
340
+ 'not_discussed': ['not_discussed', 'not discussed', 'skipped', 'not covered'],
341
  'cremation': ['cremation', 'cremate', 'ashes', 'cremated'],
342
  'burial': ['burial', 'bury', 'buried', 'cemetery', 'ground'],
343
  'donation': ['donation', 'donate', 'science', 'donate body'],
 
344
  'family_aware': ['family_aware', 'family aware', 'life insurance', 'insurance', 'covered'],
345
  'prepaid': ['prepaid', 'pre-paid', 'pre paid', 'paid'],
346
+ 'needs_discussion': ['needs_discussion', 'need to discuss'],
347
  }
348
+
349
  for opt in valid_options:
350
  if opt in matching_rules:
351
  for pattern in matching_rules[opt]:
352
  if pattern in val_str or pattern in val_normalized:
353
  return opt
354
+
355
  return default
356
 
357
 
 
359
  """Normalize all single-select fields in the extracted data."""
360
  if not data:
361
  return data
362
+
363
+ # Ensure topics_discussed exists
364
+ if 'topics_discussed' not in data:
365
+ data['topics_discussed'] = ['health', 'financial', 'funeral']
366
+
367
  # Health care wishes
368
  if 'health_care_wishes' in data:
369
  hcw = data['health_care_wishes']
 
374
  )
375
  hcw['treatment_preference'] = normalize_value(
376
  hcw.get('treatment_preference'),
377
+ ['comfort_care_only', 'full_treatment_if_recovery', 'conditional_comfort_care', 'unsure'],
378
+ 'unsure'
379
  )
380
+
381
  # Financial planning
382
  if 'financial_planning' in data:
383
  fp = data['financial_planning']
 
396
  ['yes_written', 'yes_not_written', 'not_yet'],
397
  'yes_not_written'
398
  )
399
+ fp['specific_items_status'] = normalize_value(
400
+ fp.get('specific_items_status'),
401
+ ['has_specific_items', 'no_specific_items', 'not_yet_decided'],
402
+ 'not_yet_decided'
403
+ )
404
+
405
  # Funeral plans
406
  if 'funeral_plans' in data:
407
  fun = data['funeral_plans']
408
  fun['service_type'] = normalize_value(
409
  fun.get('service_type'),
410
+ ['funeral', 'memorial', 'celebration_of_life', 'other', 'not_discussed'],
411
+ 'not_discussed'
412
  )
413
  fun['body_preference'] = normalize_value(
414
  fun.get('body_preference'),
415
+ ['burial', 'cremation', 'donation', 'undecided', 'not_discussed'],
416
+ 'not_discussed'
417
  )
418
  fun['cost_planning'] = normalize_value(
419
  fun.get('cost_planning'),
420
+ ['prepaid', 'family_aware', 'needs_discussion', 'not_discussed'],
421
+ 'not_discussed'
422
  )
423
+
424
+ # Ensure recommended_next_steps exists with sensible defaults
425
+ if 'recommended_next_steps' not in data:
426
+ data['recommended_next_steps'] = _infer_next_steps(data)
427
+
428
  return data
429
 
430
+
431
+ def _infer_next_steps(data: dict) -> dict:
432
+ """Infer recommended next steps from the extracted data when the model
433
+ does not return them explicitly."""
434
+ topics = data.get('topics_discussed', [])
435
+ health = data.get('health_care_wishes', {})
436
+ financial = data.get('financial_planning', {})
437
+
438
+ steps = {
439
+ "create_healthcare_poa": False,
440
+ "provide_poa_to_healthcare_team": False,
441
+ "complete_financial_poa_will_trust": False,
442
+ "review_update_beneficiaries": False,
443
+ "create_financial_info_list": False,
444
+ "discuss_wishes_with_loved_ones": False,
445
+ "store_documents_safely": False,
446
+ "review_plans_annually": False,
447
+ "explore_funeral_preplanning": False,
448
+ "other_steps": []
449
+ }
450
+
451
+ if 'health' in topics:
452
+ status = health.get('advance_care_status', '')
453
+ if status in ('no_documents', 'has_documents_needs_update'):
454
+ steps['create_healthcare_poa'] = True
455
+ steps['provide_poa_to_healthcare_team'] = True
456
+ steps['store_documents_safely'] = True
457
+ steps['review_plans_annually'] = True
458
+
459
+ if 'financial' in topics:
460
+ docs = financial.get('documents_in_place', {})
461
+ if is_true(docs.get('none')) or not any(
462
+ is_true(docs.get(k)) for k in
463
+ ['financial_poa', 'will', 'living_trust', 'tod_designations', 'joint_ownership']
464
+ ):
465
+ steps['complete_financial_poa_will_trust'] = True
466
+
467
+ ben = financial.get('beneficiary_status', '')
468
+ if ben in ('unsure', 'need_to_update'):
469
+ steps['review_update_beneficiaries'] = True
470
+
471
+ info = financial.get('has_info_list', '')
472
+ if info in ('not_created', 'yes_not_shared'):
473
+ steps['create_financial_info_list'] = True
474
+
475
+ shared = financial.get('shared_with_loved_ones', '')
476
+ if shared in ('not_yet', 'yes_not_written'):
477
+ steps['discuss_wishes_with_loved_ones'] = True
478
+
479
+ steps['store_documents_safely'] = True
480
+ steps['review_plans_annually'] = True
481
+
482
+ if 'funeral' in topics:
483
+ steps['explore_funeral_preplanning'] = True
484
+
485
+ return steps
486
+
487
  # ============================================================================
488
  # WORD DOCUMENT GENERATION
489
  # ============================================================================
 
585
  def generate_docx(data: dict, output_path: str) -> str:
586
  """Generate the planning summary Word document."""
587
  doc = Document()
588
+
589
  # Set default font
590
  style = doc.styles['Normal']
591
  style.font.name = 'Arial'
592
  style.font.size = Pt(10)
593
+
594
  # Set page margins (0.75 inch)
595
  for section in doc.sections:
596
  section.top_margin = Inches(0.6)
597
  section.bottom_margin = Inches(0.6)
598
  section.left_margin = Inches(0.75)
599
  section.right_margin = Inches(0.75)
600
+
601
+ topics = data.get('topics_discussed', ['health', 'financial', 'funeral'])
602
+
603
  # ===== TITLE =====
604
  title = doc.add_paragraph()
605
  title.alignment = WD_ALIGN_PARAGRAPH.CENTER
 
607
  title_run.font.name = 'Arial'
608
  title_run.font.size = Pt(16)
609
  title_run.bold = True
610
+
611
  participant = data.get('participant', {})
612
+
613
  # Participant info
614
  p = doc.add_paragraph()
615
  p.add_run("Participant Name: ").bold = True
616
  p.add_run(get_value(participant, 'name'))
617
+
618
  p = doc.add_paragraph()
619
  p.add_run("Date of Conversation: ").bold = True
620
  p.add_run(get_value(participant, 'conversation_date'))
621
+
622
  p = doc.add_paragraph()
623
  p.add_run("Facilitator: ").bold = True
624
  p.add_run(get_value(participant, 'facilitator'))
625
+
626
  p = doc.add_paragraph()
627
  p.add_run("Location: ").bold = True
628
  p.add_run(get_value(participant, 'location'))
629
+
630
  # Intro text
631
  intro = doc.add_paragraph()
632
  intro.paragraph_format.space_before = Pt(8)
 
637
  )
638
  intro_run.font.size = Pt(9)
639
  intro_run.font.color.rgb = None # Use default color
640
+
641
  # ===== HEALTH & CARE WISHES =====
642
+ if 'health' in topics:
643
+ add_section_header(doc, "My Health & Care Wishes")
644
+
645
+ health = data.get('health_care_wishes', {})
646
+
647
+ add_field_label(doc, "Who would you trust to make health decisions for you if you could not speak for yourself?")
648
+
649
+ primary = health.get('primary_decision_maker', {})
650
+ add_field_value(doc, f"Primary: {get_value(primary, 'name')} \u2013 {get_value(primary, 'relationship')}")
651
+ add_field_value(doc, f"Phone: {get_value(primary, 'phone')} | Email: {get_value(primary, 'email')}")
652
+
653
+ backup = health.get('backup_decision_maker', {})
654
+ add_field_value(doc, f"Back-up: {get_value(backup, 'name')} \u2013 {get_value(backup, 'relationship')}")
655
+ add_field_value(doc, f"Phone: {get_value(backup, 'phone')}")
656
+
657
+ add_field_label(doc, "Summary of what you shared about your values and care priorities:")
658
+ add_field_value(doc, get_value(health, 'values_summary'))
659
+
660
+ add_field_label(doc, "Current advance-care planning status:")
661
+ status = health.get('advance_care_status', '')
662
+ add_checkbox_paragraph(doc, status == 'has_current_documents', "I have a health-care power of attorney or living will (up-to-date)")
663
+ add_checkbox_paragraph(doc, status == 'has_documents_needs_update', "I have documents but need to review/update")
664
+ add_checkbox_paragraph(doc, status == 'no_documents', "I have not yet completed these documents")
665
+
666
+ add_field_label(doc, "Treatment preferences we discussed:")
667
+ treatment = health.get('treatment_preference', '')
668
+ add_checkbox_paragraph(doc, treatment == 'comfort_care_only', "Comfort care only (no machines or resuscitation under any circumstances)")
669
+ add_checkbox_paragraph(doc, treatment == 'full_treatment_if_recovery', "Full medical treatment if recovery is possible")
670
+ add_checkbox_paragraph(doc, treatment == 'conditional_comfort_care', "Conditional: treatment depends on prognosis or quality of life (see details below)")
671
+ add_checkbox_paragraph(doc, treatment == 'unsure', "Unsure / would like more information")
672
+
673
+ add_field_label(doc, "Additional details related to treatment preferences that were discussed:")
674
+ add_field_value(doc, get_value(health, 'treatment_details'))
675
+
676
+ add_field_label(doc, "Notes from conversation:")
677
+ add_field_value(doc, get_value(health, 'additional_notes'))
678
+
679
  # ===== FINANCIAL PLANNING =====
680
+ if 'financial' in topics:
681
+ doc.add_page_break()
682
+ add_section_header(doc, "My Financial Planning & Legacy")
683
+
684
+ financial = data.get('financial_planning', {})
685
+
686
+ add_sub_header(doc, "A. Trusted Person(s)")
687
+ add_field_label(doc, "Who do you trust to manage your finances if you are unable?")
688
+
689
+ fin_primary = financial.get('financial_primary', {})
690
+ add_field_value(doc, f"Primary: {get_value(fin_primary, 'name')} \u2013 {get_value(fin_primary, 'relationship')}")
691
+ add_field_value(doc, f"Phone: {get_value(fin_primary, 'phone')} | Email: {get_value(fin_primary, 'email')}")
692
+
693
+ fin_backup = financial.get('financial_backup', {})
694
+ add_field_value(doc, f"Back-up: {get_value(fin_backup, 'name')} \u2013 {get_value(fin_backup, 'relationship')}")
695
+ add_field_value(doc, f"Phone: {get_value(fin_backup, 'phone')}")
696
+
697
+ add_field_label(doc, "Conversation summary:")
698
+ add_field_value(doc, get_value(financial, 'financial_conversation_summary'))
699
+
700
+ add_sub_header(doc, "B. Legal and Financial Readiness")
701
+ add_field_label(doc, "Documents currently in place (check all that apply):")
702
+
703
+ docs = financial.get('documents_in_place', {})
704
+ add_checkbox_paragraph(doc, is_true(docs.get('financial_poa')), "Financial Power of Attorney (POA)")
705
+ add_checkbox_paragraph(doc, is_true(docs.get('will')), "Will")
706
+ add_checkbox_paragraph(doc, is_true(docs.get('living_trust')), "Living Trust")
707
+ add_checkbox_paragraph(doc, is_true(docs.get('tod_designations')), "Transfer on Death (TOD) designations")
708
+ add_checkbox_paragraph(doc, is_true(docs.get('joint_ownership')), "Joint ownership of key accounts")
709
+ add_checkbox_paragraph(doc, is_true(docs.get('none')), "None completed yet")
710
+
711
+ add_field_label(doc, "Details from conversation:")
712
+ add_field_value(doc, get_value(financial, 'legal_details'))
713
+
714
+ add_field_label(doc, "Next steps (as discussed):")
715
+ next_steps = financial.get('next_steps', {})
716
+ add_checkbox_paragraph(doc, is_true(next_steps.get('review_update_poa_will')), "Review or update existing POA or Will")
717
+ add_checkbox_paragraph(doc, is_true(next_steps.get('identify_alternate')), "Identify alternate decision-maker")
718
+ add_checkbox_paragraph(doc, is_true(next_steps.get('contact_attorney')), "Contact attorney or legal aid for document preparation")
719
+ add_checkbox_paragraph(doc, is_true(next_steps.get('seek_trust_advice')), "Seek advice on creating or updating a Trust")
720
+ other_steps = next_steps.get('other')
721
+ if other_steps and other_steps not in ('null', None, 'Not discussed', ''):
722
+ add_checkbox_paragraph(doc, True, f"Other: {other_steps}")
723
+
724
+ add_sub_header(doc, "C. Beneficiaries and Account Management")
725
+ add_field_label(doc, "Status of major accounts and beneficiaries:")
726
+
727
+ ben_status = financial.get('beneficiary_status', '')
728
+ add_checkbox_paragraph(doc, ben_status == 'all_current', "All beneficiaries current and reflect my wishes")
729
+ add_checkbox_paragraph(doc, ben_status == 'need_to_update', "Need to review or update some")
730
+ add_checkbox_paragraph(doc, ben_status == 'unsure', "Unsure / need help locating information")
731
+
732
+ add_field_label(doc, "Notes about specific accounts (bank, insurance, retirement):")
733
+ add_field_value(doc, get_value(financial, 'account_notes'))
734
+
735
+ add_field_label(doc, "Conversation summary:")
736
+ add_field_value(doc, get_value(financial, 'beneficiary_conversation'))
737
+
738
+ add_sub_header(doc, "D. Organizing Financial Information")
739
+ add_field_label(doc, "Do you have a list of key information (accounts, passwords, insurance details)?")
740
+
741
+ info_status = financial.get('has_info_list', '')
742
+ add_checkbox_paragraph(doc, info_status == 'yes_shared', "Yes \u2013 my trusted person knows where it is")
743
+ add_checkbox_paragraph(doc, info_status == 'yes_not_shared', "Yes \u2013 but not shared or outdated")
744
+ add_checkbox_paragraph(doc, info_status == 'not_created', "Not yet created")
745
+
746
+ add_field_label(doc, "Where this information can be found:")
747
+ add_field_value(doc, get_value(financial, 'info_location'))
748
+
749
+ add_field_label(doc, "Additional organization ideas shared in conversation:")
750
+ add_field_value(doc, get_value(financial, 'organization_ideas'))
751
+
752
+ add_sub_header(doc, "E. Talking with Loved Ones")
753
+ add_field_label(doc, "Have you shared your financial wishes with loved ones or trusted helpers?")
754
+
755
+ shared = financial.get('shared_with_loved_ones', '')
756
+ add_checkbox_paragraph(doc, shared == 'yes_written', "Yes \u2013 discussed and written down")
757
+ add_checkbox_paragraph(doc, shared == 'yes_not_written', "Yes \u2013 discussed, not written")
758
+ add_checkbox_paragraph(doc, shared == 'not_yet', "Not yet")
759
+
760
+ add_field_label(doc, "Notes from discussion:")
761
+ add_field_value(doc, get_value(financial, 'sharing_notes'))
762
+
763
+ add_sub_header(doc, "F. Overall Financial Wishes and Legacy Intentions")
764
+ add_field_label(doc, "Summary of what matters most to you about how your financial affairs are handled:")
765
+ add_field_value(doc, get_value(financial, 'overall_wishes'))
766
+
767
+ add_field_label(doc, "Are there personal or sentimental items you want to designate for specific people?")
768
+ items_status = financial.get('specific_items_status', 'not_yet_decided')
769
+ add_checkbox_paragraph(doc, items_status == 'has_specific_items', "Yes (list below)")
770
+ add_checkbox_paragraph(doc, items_status == 'no_specific_items', "No specific designations needed")
771
+ add_checkbox_paragraph(doc, items_status == 'not_yet_decided', "Not yet decided")
772
+
773
+ items = financial.get('specific_items', [])
774
+ if items and items_status == 'has_specific_items':
775
+ add_field_label(doc, "Items / recipients:")
776
+ for item in items:
777
+ add_field_value(doc, f"\u2013 {item.get('item', 'Item')} \u2192 {item.get('recipient', 'Recipient')}")
778
+
779
  # ===== FUNERAL & MEMORIAL =====
780
+ if 'funeral' in topics:
781
+ doc.add_page_break()
782
+ add_section_header(doc, "My Funeral & Memorial Plans")
783
+
784
+ funeral = data.get('funeral_plans', {})
785
+
786
+ add_field_label(doc, "Type of service you prefer:")
787
+ service_type = funeral.get('service_type', '')
788
+ other_service = funeral.get('service_type_other') or '____________________'
789
+ add_checkbox_paragraph(doc, service_type == 'funeral', "Funeral service")
790
+ add_checkbox_paragraph(doc, service_type == 'memorial', "Memorial service")
791
+ add_checkbox_paragraph(doc, service_type == 'celebration_of_life', "Celebration of Life")
792
+ add_checkbox_paragraph(doc, service_type == 'other', f"Other: {other_service}")
793
+
794
+ add_field_label(doc, "Body preference:")
795
+ body_pref = funeral.get('body_preference', '')
796
+ body_details = funeral.get('body_details') or ''
797
+
798
+ burial_loc = body_details if body_pref == 'burial' and body_details else '____________________'
799
+ crem_details = body_details if body_pref == 'cremation' and body_details else '____________________'
800
+
801
+ add_checkbox_paragraph(doc, body_pref == 'burial', f"Burial (location: {burial_loc})")
802
+ add_checkbox_paragraph(doc, body_pref == 'cremation', f"Cremation ({crem_details})")
803
+ add_checkbox_paragraph(doc, body_pref == 'donation', "Donation to science")
804
+ add_checkbox_paragraph(doc, body_pref == 'undecided', "Undecided")
805
+
806
+ add_field_label(doc, "Summary of conversation:")
807
+ add_field_value(doc, get_value(funeral, 'conversation_summary'))
808
+
809
+ add_field_label(doc, "Special requests or details:")
810
+
811
+ p = doc.add_paragraph()
812
+ p.add_run("Preferred location: ").bold = True
813
+ p.add_run(get_value(funeral, 'preferred_location'))
814
+
815
+ p = doc.add_paragraph()
816
+ p.add_run("Leader of service: ").bold = True
817
+ p.add_run(get_value(funeral, 'service_leader'))
818
+
819
+ p = doc.add_paragraph()
820
+ p.add_run("Music/Readings: ").bold = True
821
+ p.add_run(get_value(funeral, 'music_readings'))
822
+
823
+ p = doc.add_paragraph()
824
+ p.add_run("Appearance/Clothing: ").bold = True
825
+ p.add_run(get_value(funeral, 'appearance_clothing'))
826
+
827
+ p = doc.add_paragraph()
828
+ p.add_run("Charities for donations: ").bold = True
829
+ p.add_run(get_value(funeral, 'charity_donations'))
830
+
831
+ add_field_label(doc, "Funeral cost planning:")
832
+ cost = funeral.get('cost_planning', '')
833
+ add_checkbox_paragraph(doc, cost == 'prepaid', "Pre-paid plan")
834
+ add_checkbox_paragraph(doc, cost == 'family_aware', "Family aware of funding")
835
+ add_checkbox_paragraph(doc, cost == 'needs_discussion', "Needs discussion")
836
+
837
+ add_field_label(doc, "Additional notes:")
838
+ add_field_value(doc, get_value(funeral, 'additional_notes'))
839
+ else:
840
+ # Funeral not discussed: add a brief note instead of the full section
841
+ doc.add_page_break()
842
+ add_section_header(doc, "My Funeral & Memorial Plans")
843
+ add_field_value(doc, "Funeral and memorial planning was not discussed in this session.")
844
+
845
  # ===== VALUES & REFLECTIONS =====
846
  add_section_header(doc, "My Values & Life Reflections")
847
+
848
  values = data.get('values_reflections', {})
849
+
850
  add_field_label(doc, "What matters most to me about how I live and am remembered:")
851
  add_field_value(doc, get_value(values, 'what_matters_most'))
852
+
853
  add_field_label(doc, "What gives my life meaning and joy:")
854
  add_field_value(doc, get_value(values, 'meaning_and_joy'))
855
+
856
  add_field_label(doc, "What I hope my family and friends remember most:")
857
  add_field_value(doc, get_value(values, 'want_remembered_for'))
858
+
859
+ # ===== NEXT STEPS (data-driven) =====
860
  add_section_header(doc, "Next Steps & Resources")
861
  add_field_label(doc, "From today's conversation, the next steps we identified:")
862
+
863
+ rec = data.get('recommended_next_steps', {})
864
+
865
+ step_labels = [
866
+ ('create_healthcare_poa', "Update or create a Health Care Power of Attorney (POA) or Living Will"),
867
+ ('provide_poa_to_healthcare_team', "Provide copies of my Health Care POA and Living Will to my health care team"),
868
+ ('complete_financial_poa_will_trust', "Complete or update Financial Power of Attorney / Will / Trust"),
869
+ ('review_update_beneficiaries', "Review and update beneficiaries on insurance, retirement, and bank accounts"),
870
+ ('create_financial_info_list', "Create or update a list of key financial information and tell my trusted person where it's stored"),
871
+ ('discuss_wishes_with_loved_ones', "Talk with my loved ones about my wishes for health, finances, and funeral planning"),
872
+ ('store_documents_safely', "Store all important documents safely in a clearly labeled folder or binder at home"),
873
+ ('review_plans_annually', "Review all plans annually or after major life events"),
874
+ ('explore_funeral_preplanning', "Explore funeral or memorial pre-planning options"),
875
+ ]
876
+
877
+ for key, label in step_labels:
878
+ checked = is_true(rec.get(key, False))
879
+ add_checkbox_paragraph(doc, checked, label)
880
+
881
+ # Handle additional custom steps
882
+ other_steps = rec.get('other_steps', [])
883
+ if isinstance(other_steps, list):
884
+ for step_text in other_steps:
885
+ if step_text and step_text not in ('null', None, ''):
886
+ add_checkbox_paragraph(doc, True, step_text)
887
+ elif isinstance(other_steps, str) and other_steps not in ('null', None, ''):
888
+ add_checkbox_paragraph(doc, True, other_steps)
889
+
890
  add_field_label(doc, "Facilitator Summary or Recommendations:")
891
  add_field_value(doc, get_value(data, 'facilitator_summary'))
892
+
893
  doc.save(output_path)
894
  return output_path
895
 
 
901
  """Main function to process audio and generate Word document."""
902
  if audio_file is None:
903
  return None, "Please record or upload an audio file.", None
904
+
905
  api_key = os.environ.get("GEMINI_API_KEY")
906
  if not api_key:
907
  return None, "API key not configured. Please set GEMINI_API_KEY in Space secrets.", None
908
+
909
  try:
910
  # Analyze audio
911
  raw_response = analyze_audio(audio_file, api_key)
912
+
913
  # Parse response
914
  data = parse_json_response(raw_response)
915
  if not data:
916
  return None, "Failed to parse the AI response. Please try again.", None
917
+
918
  # Normalize data
919
  data = normalize_data(data)
920
+
921
  # Generate Word document
922
  participant_name = get_value(data, 'participant', 'name', default='Unknown')
923
  safe_name = re.sub(r'[^a-zA-Z0-9]', '_', participant_name)
924
+
925
  output_dir = tempfile.gettempdir()
926
  output_filename = os.path.join(output_dir, f"Planning_Summary_{safe_name}.docx")
927
+
928
  generate_docx(data, output_filename)
929
+
930
  # Return results
931
  json_output = json.dumps(data, indent=2)
932
  status = f"Successfully generated planning summary for {participant_name}"
933
+
934
  return output_filename, status, json_output
935
+
936
  except Exception as e:
937
  return None, f"Error: {str(e)}", None
938
 
 
944
  """
945
  if audio_data is None:
946
  return None, "No audio recorded.", None
947
+
948
  # Save the recorded audio to a temporary file
949
  import numpy as np
950
  from scipy.io import wavfile
951
+
952
  sample_rate, audio_array = audio_data
953
+
954
  # Create temporary wav file
955
  temp_dir = tempfile.gettempdir()
956
  temp_path = os.path.join(temp_dir, f"recording_{int(time.time())}.wav")
957
+
958
  # Ensure audio is in the right format
959
  if audio_array.dtype != np.int16:
960
  # Normalize and convert to int16
 
962
  audio_array = (audio_array * 32767).astype(np.int16)
963
  else:
964
  audio_array = audio_array.astype(np.int16)
965
+
966
  wavfile.write(temp_path, sample_rate, audio_array)
967
+
968
  # Process the audio
969
  docx_file, status, json_data = process_audio(temp_path)
970
+
971
  return docx_file, status, json_data
972
 
973
 
 
975
  """Process an uploaded audio file."""
976
  if audio_file is None:
977
  return None, "Please upload an audio file.", None
978
+
979
  return process_audio(audio_file)
980
 
981
  # ============================================================================
982
  # GRADIO INTERFACE
983
  # ============================================================================
984
 
985
+ if HAS_GRADIO:
986
+ # Custom theme with neutral colors
987
+ custom_theme = gr.themes.Base(
988
+ primary_hue=gr.themes.colors.slate,
989
+ secondary_hue=gr.themes.colors.gray,
990
+ neutral_hue=gr.themes.colors.gray,
991
+ ).set(
992
+ button_primary_background_fill="#1a1a1a",
993
+ button_primary_background_fill_hover="#333333",
994
+ button_primary_text_color="white",
995
+ block_label_text_color="#374151",
996
+ block_title_text_color="#111827",
997
+ )
998
+
999
+ with gr.Blocks(title="Advance Care Planning") as demo:
1000
+ gr.Markdown("""
1001
+ # Advance Care Planning
1002
+
1003
+ Record or upload an audio conversation to generate a structured Word document summary report.
1004
+ """)
1005
+
1006
+ with gr.Tabs():
1007
+ with gr.TabItem("Record Audio"):
1008
+ gr.Markdown("""
1009
+ **Instructions:** Click the microphone button to start recording. Click again to stop.
1010
+ The recording will be automatically analyzed when you stop.
1011
+ """)
1012
+
1013
+ with gr.Row():
1014
+ with gr.Column(scale=1):
1015
+ audio_recorder = gr.Audio(
1016
+ label="Recording",
1017
+ sources=["microphone"],
1018
+ type="numpy",
1019
+ interactive=True
1020
+ )
1021
+
1022
+ with gr.Column(scale=1):
1023
+ record_status = gr.Textbox(label="Status", interactive=False)
1024
+ record_docx_output = gr.File(label="Download Word Document")
1025
+
1026
+ with gr.Accordion("View Extracted Data (JSON)", open=False):
1027
+ record_json_output = gr.Code(label="Extracted Data", language="json")
1028
+
1029
+ # Auto-process when recording stops
1030
+ audio_recorder.stop_recording(
1031
+ fn=on_recording_stop,
1032
+ inputs=[audio_recorder],
1033
+ outputs=[record_docx_output, record_status, record_json_output]
1034
+ )
1035
+
1036
+ with gr.TabItem("Upload Audio"):
1037
+ with gr.Row():
1038
+ with gr.Column(scale=1):
1039
+ audio_upload = gr.Audio(
1040
+ label="Upload Audio Recording",
1041
+ type="filepath",
1042
+ sources=["upload"]
1043
+ )
1044
+
1045
+ upload_btn = gr.Button("Analyze & Generate Word Doc", variant="primary")
1046
+
1047
+ with gr.Column(scale=1):
1048
+ upload_status = gr.Textbox(label="Status", interactive=False)
1049
+ upload_docx_output = gr.File(label="Download Word Document")
1050
+
1051
+ with gr.Accordion("View Extracted Data (JSON)", open=False):
1052
+ upload_json_output = gr.Code(label="Extracted Data", language="json")
1053
+
1054
+ upload_btn.click(
1055
+ fn=process_uploaded_file,
1056
+ inputs=[audio_upload],
1057
+ outputs=[upload_docx_output, upload_status, upload_json_output]
1058
+ )
1059
+
1060
+ gr.Markdown("""
1061
+ ---
1062
+ **Notes:**
1063
+ - Supported audio formats: MP3, WAV, M4A, and other common formats
1064
+ - The generated Word document is a summary document, not a legal document
1065
+ """)
1066
 
1067
  if __name__ == "__main__":
1068
+ if HAS_GRADIO:
1069
+ demo.launch(theme=custom_theme)
1070
+ else:
1071
+ print("Gradio not installed. Core logic is available for import.")