Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
85 changes: 83 additions & 2 deletions backend/two_stage_claude_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/app/dashboard/doctor/cases/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,22 @@ export default function CaseReviewRoute() {
</span>
</div>
</div>
{/* 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 (
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-900 mb-2">Patient Notes</h4>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-gray-800 whitespace-pre-wrap">{patientNotes}</p>
</div>
</div>
)
})()}
</div>

{/* Patient Video */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'
})
})
Expand Down Expand Up @@ -882,6 +882,26 @@ export default function ExerciseDetailPage() {
Analysis Progress
</h3>

{/* Troubleshooting when pose data missing */}
{currentStep === 'complete' && analysisResult && (analysisResult as any)?.analysis_summary?.overall_health_assessment?.toLowerCase().includes('insufficient pose data') && (
<div style={{
backgroundColor: 'rgba(251, 191, 36, 0.1)',
border: '1px solid rgba(251, 191, 36, 0.4)',
color: '#92400e',
borderRadius: '6px',
padding: '8px',
marginBottom: '8px'
}}>
<div style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: '4px' }}>No pose data captured</div>
<ul style={{ margin: 0, paddingLeft: '18px', fontSize: '0.75rem' }}>
<li>Ensure good lighting and full body in frame</li>
<li>Place camera horizontally and stable</li>
<li>Wear contrasting clothing against background</li>
<li>Retry recording and re-upload</li>
</ul>
</div>
)}

{/* Current Step */}
<div style={{ marginBottom: '8px' }}>
<div style={{
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ function createPatientCaseFromSession(s: any, treatmentsById: any): PatientCase
painIndicators: aiAnalysis.painIndicators.map(p => `${p.location}: ${p.type} pain (${p.severity}/10)`),
};

// Surface patient notes for doctor view downstream
(pc as any).patientNotes = s.patient_notes || ''

return pc;
}

Expand Down