diff --git a/backend/main.py b/backend/main.py index 586f785..89a21e1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -503,11 +503,21 @@ async def perform_two_stage_analysis(video_id: str): key_frames_dir = OUTPUT_DIR / f"{video_id}_key_frames" key_frames_dir.mkdir(exist_ok=True) action_logger.log_file_operation("CREATE_DIR", key_frames_dir, True) - - # Extract key frames + + # Build full analysis package including pose data from angles file action_logger.log_processing_step("KEY_FRAME_EXTRACTION", video_id, "started") - analysis_package = key_frame_extractor.extract_key_frames( - str(video_path), + try: + with open(angle_file, 'r') as f: + angle_payload = json.load(f) + angle_data = angle_payload.get('angle_data', []) + except Exception as e: + action_logger.log_error("ANGLE_DATA_READ_FAILED", str(e), {"video_id": video_id}) + raise HTTPException(status_code=500, detail=f"Failed to read angle data: {str(e)}") + + # Use enhanced extractor to attach pose data to frames and summarize angles + analysis_package = key_frame_extractor.create_analysis_package( + str(video_path), + angle_data, str(key_frames_dir) ) action_logger.log_processing_step("KEY_FRAME_EXTRACTION", video_id, "completed") diff --git a/backend/two_stage_claude_analyzer.py b/backend/two_stage_claude_analyzer.py index 60863c9..63fecdc 100644 --- a/backend/two_stage_claude_analyzer.py +++ b/backend/two_stage_claude_analyzer.py @@ -86,16 +86,29 @@ def _analyze_movement_overview(self, analysis_package: Dict) -> Dict: 'interval': frame.get('interval', ''), 'has_pose_data': frame.get('has_pose_data', False) } - + # Add pose data if available if frame.get('pose_data'): frame_data['pose_data'] = frame['pose_data'] - + key_frame_data.append(frame_data) # Create prompt for movement overview prompt = self._create_movement_overview_prompt(video_info, key_frame_data) + # If no images or key frames, return a graceful placeholder + if not key_frames or all(not f.get('image_encoded') for f in key_frames): + return { + 'movement_type': 'unknown', + 'confidence': 0.0, + 'movement_description': 'Unable to parse response', + 'technique_quality': 'unknown', + 'key_observations': [], + 'obvious_concerns': [], + 'overall_assessment': 'Analysis failed', + 'recommendations': [] + } + # Call Claude API response = self._call_claude_api(prompt, "movement_overview") @@ -134,6 +147,74 @@ def _analyze_health_with_pose_data(self, analysis_package: Dict, movement_overvi movement_confidence ) + # If no pose data at all, return a structured "insufficient data" report + angle_summary = pose_analysis.get('angle_summary') or {} + no_pose = (not angle_summary) and (not pose_analysis.get('pose_data_available')) and not any( + f.get('has_pose_data') for f in key_frames + ) + if no_pose: + return { + 'overall_assessment': 'Unable to provide a detailed assessment due to insufficient pose data. No movement data or pose information was captured in the provided frames, making it impossible to conduct a thorough biomechanical analysis.', + 'movement_analysis': { + 'movement_type': 'unknown', + 'technique_quality': 'unable to assess', + 'movement_efficiency': 'unable to assess', + 'key_findings': [ + 'No pose data available for analysis', + 'Movement type could not be determined' + ], + 'concerns': [ + 'Unable to assess movement patterns due to missing data' + ], + 'recommendations': [ + 'Recapture movement data with proper pose tracking', + 'Ensure recording equipment is functioning correctly', + 'Verify sensor placement and calibration' + ] + }, + 'biomechanical_analysis': { + 'joint_analysis': { + 'knee': 'No data available for analysis', + 'hip': 'No data available for analysis', + 'ankle': 'No data available for analysis', + 'spine': 'No data available for analysis', + 'overall': 'Cannot perform biomechanical assessment without pose data' + }, + 'posture_assessment': 'Unable to assess posture without pose data', + 'movement_patterns': 'No movement patterns detected', + 'asymmetries': 'Cannot evaluate asymmetries without comparative data' + }, + 'health_insights': { + 'strengths': ['Cannot identify strengths without movement data'], + 'areas_for_improvement': ['Data capture and tracking system functionality', 'Movement recording protocol'], + 'risk_factors': ['Unable to assess physical risk factors', 'Technical limitations preventing proper assessment'], + 'positive_findings': ['No positive findings can be determined without data'] + }, + 'recommendations': { + 'immediate_actions': [ + 'Check recording equipment functionality', + 'Verify pose tracking system is properly calibrated', + 'Ensure adequate lighting and camera positioning', + 'Repeat movement capture with working system' + ], + 'long_term_goals': [ + 'Establish reliable movement capture protocol', + 'Implement regular system maintenance checks', + 'Create backup recording procedures' + ], + 'exercises': ['Cannot provide specific exercise recommendations without movement analysis'], + 'precautions': [ + 'Do not base any treatment decisions on this incomplete analysis', + 'Seek proper assessment with functioning measurement tools' + ] + }, + 'follow_up': { + 'next_steps': ['Troubleshoot recording system', 'Schedule new movement assessment session', 'Verify all equipment is functioning before next attempt'], + 'monitoring': ['System functionality checks before recording', 'Data capture quality during recording', 'Proper pose tracking throughout movement'], + 'when_to_seek_help': 'Immediately consult with technical support to resolve data capture issues before attempting another movement analysis' + } + } + # Call Claude API response = self._call_claude_api(prompt, "health_analysis") diff --git a/frontend/src/app/dashboard/doctor/cases/[id]/page.tsx b/frontend/src/app/dashboard/doctor/cases/[id]/page.tsx index 0f0136c..63a883d 100644 --- a/frontend/src/app/dashboard/doctor/cases/[id]/page.tsx +++ b/frontend/src/app/dashboard/doctor/cases/[id]/page.tsx @@ -239,6 +239,22 @@ export default function CaseReviewRoute() { + {/* Patient Notes surfaced for doctor */} + {(() => { + // Attempt to surface patient notes from sessions table via doctor API backing data + // doctorApi.getCaseById builds from /api/doctor/sessions which includes patient_notes + const anyCase: any = caseData as any + const patientNotes: string | undefined = anyCase.patientNotes || anyCase.patient_notes + if (!patientNotes || !patientNotes.trim()) return null + return ( +
+

Patient Notes

+
+

{patientNotes}

+
+
+ ) + })()} {/* Patient Video */} diff --git a/frontend/src/app/dashboard/patient/session/[sessionId]/[exerciseName]/page.tsx b/frontend/src/app/dashboard/patient/session/[sessionId]/[exerciseName]/page.tsx index 4f04a7d..fdc48c4 100644 --- a/frontend/src/app/dashboard/patient/session/[sessionId]/[exerciseName]/page.tsx +++ b/frontend/src/app/dashboard/patient/session/[sessionId]/[exerciseName]/page.tsx @@ -426,7 +426,7 @@ export default function ExerciseDetailPage() { const result = await analysisResponse.json() console.log('🎉 Claude analysis result:', result) - setAnalysisResult(result) + setAnalysisResult(result?.analysis || result) if (result.key_frames) { setKeyFrames(result.key_frames) @@ -439,7 +439,7 @@ export default function ExerciseDetailPage() { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - ai_evaluation: result, // Save as JSONB object, not stringified + ai_evaluation: result?.analysis ? result : { analysis: result }, // normalize to object with analysis key status: 'completed' }) }) @@ -882,6 +882,26 @@ export default function ExerciseDetailPage() { Analysis Progress + {/* Troubleshooting when pose data missing */} + {currentStep === 'complete' && analysisResult && (analysisResult as any)?.analysis_summary?.overall_health_assessment?.toLowerCase().includes('insufficient pose data') && ( +
+
No pose data captured
+ +
+ )} + {/* Current Step */}
`${p.location}: ${p.type} pain (${p.severity}/10)`), }; + // Surface patient notes for doctor view downstream + (pc as any).patientNotes = s.patient_notes || '' + return pc; }