SmartHeal commited on
Commit
c421c59
·
verified ·
1 Parent(s): da9c442

Update src/ai_processor.py

Browse files
Files changed (1) hide show
  1. src/ai_processor.py +138 -172
src/ai_processor.py CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  import io
2
  import base64
3
  import logging
@@ -15,7 +19,6 @@ from langchain_community.vectorstores import FAISS
15
  from huggingface_hub import HfApi, HfFolder
16
  import spaces
17
  from .config import Config
18
- import os
19
 
20
  default_system_prompt = (
21
  "You are a world-class medical AI assistant specializing in wound care "
@@ -38,37 +41,28 @@ class AIProcessor:
38
  self._initialize_models()
39
 
40
  def _initialize_models(self):
41
- """Initialize CPU-only AI models; MedGemma is loaded on demand within GPU context."""
42
- # Set HF token
43
  if self.config.HF_TOKEN:
44
  HfFolder.save_token(self.config.HF_TOKEN)
45
- logging.info("HuggingFace token set successfully")
46
 
47
- # YOLO detection on CPU (force CPU)
48
  try:
49
- self.models_cache['det'] = YOLO(
50
- self.config.YOLO_MODEL_PATH,
51
- device='cpu'
52
- )
53
- logging.info("✅ YOLO detection model loaded on CPU")
54
  except Exception as e:
55
- logging.error(
56
- f"Failed to load YOLO model at '{self.config.YOLO_MODEL_PATH}': {e}"
57
- )
58
- # fail fast so you’ll immediately see why 'det' was never set
59
  raise
60
 
61
- # Segmentation model on CPU
62
  try:
63
- self.models_cache['seg'] = load_model(
64
- self.config.SEG_MODEL_PATH,
65
- compile=False
66
- )
67
- logging.info("✅ Segmentation model loaded on CPU")
68
  except Exception as e:
69
  logging.warning(f"Segmentation model not available: {e}")
70
 
71
- # Classification on CPU
72
  try:
73
  self.models_cache['cls'] = pipeline(
74
  'image-classification',
@@ -76,21 +70,21 @@ class AIProcessor:
76
  token=self.config.HF_TOKEN,
77
  device='cpu'
78
  )
79
- logging.info("✅ Wound classification model loaded on CPU")
80
  except Exception as e:
81
- logging.warning(f"Wound classification model not available: {e}")
82
 
83
- # Embeddings for KB on CPU
84
  try:
85
  self.models_cache['embedding_model'] = HuggingFaceEmbeddings(
86
  model_name='sentence-transformers/all-MiniLM-L6-v2',
87
  model_kwargs={'device': 'cpu'}
88
  )
89
- logging.info("✅ Embedding model loaded on CPU")
90
  except Exception as e:
91
  logging.warning(f"Embedding model not available: {e}")
92
 
93
- # Load PDF guidelines
94
  self._load_knowledge_base()
95
 
96
  def _load_knowledge_base(self):
@@ -99,118 +93,80 @@ class AIProcessor:
99
  if os.path.exists(pdf):
100
  loader = PyPDFLoader(pdf)
101
  docs.extend(loader.load())
102
- logging.info(f"Loaded PDF: {pdf}")
103
 
104
  if docs and 'embedding_model' in self.models_cache:
105
- splitter = RecursiveCharacterTextSplitter(
106
- chunk_size=1000, chunk_overlap=100
107
- )
108
  chunks = splitter.split_documents(docs)
109
- vs = FAISS.from_documents(
110
- chunks, self.models_cache['embedding_model']
111
- )
112
  self.knowledge_base_cache['vectorstore'] = vs
113
- logging.info(f"✅ Knowledge base loaded ({len(chunks)} chunks)")
114
  else:
115
  self.knowledge_base_cache['vectorstore'] = None
116
  logging.warning("Knowledge base unavailable")
117
 
118
  def perform_visual_analysis(self, image_pil):
119
- """Detect & segment on CPU; return only paths + metrics."""
120
  if 'det' not in self.models_cache:
121
- raise RuntimeError(
122
- "YOLO detection model ('det') not loaded; cannot perform visual analysis"
123
- )
124
 
125
- try:
126
- img_cv = cv2.cvtColor(
127
- np.array(image_pil), cv2.COLOR_RGB2BGR
128
- )
129
- res = self.models_cache['det'].predict(
130
- img_cv, verbose=False
131
- )[0]
132
- if not res.boxes:
133
- raise ValueError("No wound detected")
134
 
135
- # Bounding box
136
- x1, y1, x2, y2 = res.boxes.xyxy[0].cpu().numpy().astype(int)
137
- region = img_cv[y1:y2, x1:x2]
138
 
139
- # Save detection overlay
140
- det_vis = img_cv.copy()
141
- cv2.rectangle(det_vis, (x1, y1), (x2, y2), (0, 255, 0), 2)
142
- os.makedirs(f"{self.config.UPLOADS_DIR}/analysis", exist_ok=True)
143
- ts = datetime.now().strftime('%Y%m%d_%H%M%S')
144
- det_path = (
145
- f"{self.config.UPLOADS_DIR}/analysis/detection_{ts}.png"
146
- )
147
- cv2.imwrite(det_path, det_vis)
148
 
149
- # Initialize metrics & seg
150
- length = breadth = area = 0
151
- seg_path = None
 
 
 
 
 
 
 
 
 
152
 
153
- # Segmentation (if available)
154
- if 'seg' in self.models_cache:
155
- h, w = self.models_cache['seg'].input_shape[1:3]
156
- inp = cv2.resize(region, (w, h)) / 255.0
157
- mask = (
158
- self.models_cache['seg']
159
- .predict(np.expand_dims(inp, 0))[0, :, :, 0] > 0.5
160
- ).astype(np.uint8)
161
- mask_rs = cv2.resize(
162
- mask,
163
- (region.shape[1], region.shape[0]),
164
- interpolation=cv2.INTER_NEAREST
165
- )
166
- ov = region.copy()
167
- ov[mask_rs == 1] = [0, 0, 255]
168
- seg_vis = cv2.addWeighted(region, 0.7, ov, 0.3, 0)
169
- seg_path = (
170
- f"{self.config.UPLOADS_DIR}/analysis/segmentation_{ts}.png"
171
- )
172
- cv2.imwrite(seg_path, seg_vis)
173
 
174
- cnts, _ = cv2.findContours(
175
- mask_rs,
176
- cv2.RETR_EXTERNAL,
177
- cv2.CHAIN_APPROX_SIMPLE
 
 
178
  )
179
- if cnts:
180
- cnt = max(cnts, key=cv2.contourArea)
181
- _, _, w0, h0 = cv2.boundingRect(cnt)
182
- length = round(h0 / self.px_per_cm, 2)
183
- breadth = round(w0 / self.px_per_cm, 2)
184
- area = round(
185
- cv2.contourArea(cnt) / (self.px_per_cm ** 2), 2
186
- )
187
 
188
- # Classification
189
- wound_type = 'Unknown'
190
- if 'cls' in self.models_cache:
191
- try:
192
- labels = self.models_cache['cls'](
193
- Image.fromarray(cv2.cvtColor(region, cv2.COLOR_BGR2RGB))
194
- )
195
- wound_type = max(labels, key=lambda x: x['score'])['label']
196
- except Exception:
197
- pass
198
-
199
- return {
200
- 'wound_type': wound_type,
201
- 'length_cm': length,
202
- 'breadth_cm': breadth,
203
- 'surface_area_cm2': area,
204
- 'detection_confidence': float(
205
- res.boxes.conf[0].cpu().item()
206
- ),
207
- 'detection_image_path': det_path,
208
- 'segmentation_image_path': seg_path
209
- }
210
-
211
- except Exception as e:
212
- logging.error(f"Visual analysis error: {e}")
213
- raise
214
 
215
  def query_guidelines(self, query: str):
216
  vs = self.knowledge_base_cache.get('vectorstore')
@@ -218,15 +174,12 @@ class AIProcessor:
218
  return "Clinical guidelines unavailable"
219
  docs = vs.as_retriever(search_kwargs={'k':10}).invoke(query)
220
  return '\n\n'.join(
221
- f"Source: {d.metadata.get('source','?')}, Page: {d.metadata.get('page','?')}\n"
222
- f"{d.page_content}"
223
  for d in docs
224
  )
225
 
226
  @spaces.GPU(enable_queue=True, duration=120)
227
- def generate_final_report(
228
- self, patient_info, visual_results, guideline_context, image_pil, max_new_tokens=None
229
- ):
230
  """Run MedGemma on GPU; return markdown report."""
231
  if 'medgemma_pipe' not in self.models_cache:
232
  try:
@@ -234,16 +187,14 @@ class AIProcessor:
234
  'image-text-to-text',
235
  model='google/medgemma-4b-it',
236
  device='auto',
237
- torch_dtype=torch.bfloat16,
238
  offload_folder='offload',
239
  token=self.config.HF_TOKEN
240
  )
241
  logging.info("✅ MedGemma pipeline loaded on GPU")
242
  except Exception as e:
243
- logging.warning(f"MedGemma pipeline not available: {e}")
244
- return self._generate_fallback_report(
245
- patient_info, visual_results, guideline_context
246
- )
247
 
248
  msgs = [
249
  {'role':'system','content':[{'type':'text','text':default_system_prompt}]},
@@ -254,13 +205,8 @@ class AIProcessor:
254
  for key in ('detection_image_path','segmentation_image_path'):
255
  p = visual_results.get(key)
256
  if p and os.path.exists(p):
257
- msgs[1]['content'].append(
258
- {'type':'image','image':Image.open(p)}
259
- )
260
- prompt = (
261
- f"## Patient\n{patient_info}\n"
262
- f"## Wound Type: {visual_results['wound_type']}"
263
- )
264
  msgs[1]['content'].append({'type':'text','text':prompt})
265
 
266
  out = self.models_cache['medgemma_pipe'](
@@ -269,18 +215,14 @@ class AIProcessor:
269
  do_sample=False
270
  )
271
  report = out[0]['generated_text'][-1].get('content','')
272
- return report or self._generate_fallback_report(
273
- patient_info, visual_results, guideline_context
274
- )
275
 
276
  def _generate_fallback_report(self, patient_info, visual_results, guideline_context):
277
  dp = visual_results.get('detection_image_path','N/A')
278
  sp = visual_results.get('segmentation_image_path','N/A')
279
  return (
280
- f"# Report\n{patient_info}\n"
281
- f"Type: {visual_results['wound_type']}\n"
282
- f"Detection Image: {dp}\n"
283
- f"Segmentation Image: {sp}\n"
284
  f"Guidelines: {guideline_context[:200]}..."
285
  )
286
 
@@ -289,7 +231,7 @@ class AIProcessor:
289
  fn = f"{datetime.now():%Y%m%d_%H%M%S}.png"
290
  path = os.path.join(self.config.UPLOADS_DIR, fn)
291
  image_pil.convert('RGB').save(path)
292
- if self.config.HF_TOKEN and getattr(self.config,'DATASET_ID', None):
293
  try:
294
  api = HfApi()
295
  api.upload_file(
@@ -306,17 +248,10 @@ class AIProcessor:
306
  try:
307
  saved = self.save_and_commit_image(image_pil)
308
  vis = self.perform_visual_analysis(image_pil)
309
- info = ", ".join(
310
- f"{k}:{v}" for k,v in questionnaire_data.items() if v
311
- )
312
  gc = self.query_guidelines(info)
313
  report = self.generate_final_report(info, vis, gc, image_pil)
314
- return {
315
- 'success': True,
316
- 'visual_analysis': vis,
317
- 'report': report,
318
- 'saved_image_path': saved
319
- }
320
  except Exception as e:
321
  logging.error(f"Pipeline error: {e}")
322
  return {'success': False, 'error': str(e)}
@@ -327,27 +262,58 @@ class AIProcessor:
327
  return self.full_analysis_pipeline(image, questionnaire_data)
328
 
329
  def _assess_risk_legacy(self, questionnaire_data):
330
- risk_factors, risk_score = [], 0
 
 
 
331
  try:
 
332
  age = questionnaire_data.get('patient_age', 0)
333
  if age > 65:
334
- risk_factors.append("Advanced age (>65)"); risk_score+=2
 
335
  elif age > 50:
336
- risk_factors.append("Older adult (50-65)"); risk_score+=1
337
- duration = questionnaire_data.get('wound_duration','').lower()
338
- if any(term in duration for term in ['month','year']):
339
- risk_factors.append("Chronic wound (>4 weeks)"); risk_score+=3
340
- pain = questionnaire_data.get('pain_level',0)
341
- if pain>=7: risk_factors.append("High pain level"); risk_score+=2
342
- hist = questionnaire_data.get('medical_history','').lower()
343
- if 'diabetes' in hist: risk_factors.append("Diabetes mellitus"); risk_score+=3
344
- if 'vascular' in hist: risk_factors.append("Vascular issues"); risk_score+=2
345
- if 'immune' in hist: risk_factors.append("Immune compromise"); risk_score+=2
346
-
347
- if risk_score>=7: level="High"
348
- elif risk_score>=4: level="Moderate"
349
- else: level="Low"
350
- return {'risk_score':risk_score,'risk_level':level,'risk_factors':risk_factors}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  except Exception as e:
352
  logging.error(f"Risk assessment error: {e}")
353
- return {'risk_score':0,'risk_level':'Unknown','risk_factors':[]}
 
1
+ # Disable GPU for all CPU-only model loading to avoid triggering CUDA init in the main process
2
+ import os
3
+ os.environ['CUDA_VISIBLE_DEVICES'] = ''
4
+
5
  import io
6
  import base64
7
  import logging
 
19
  from huggingface_hub import HfApi, HfFolder
20
  import spaces
21
  from .config import Config
 
22
 
23
  default_system_prompt = (
24
  "You are a world-class medical AI assistant specializing in wound care "
 
41
  self._initialize_models()
42
 
43
  def _initialize_models(self):
44
+ """Initialize all CPU-only models here; GPU models loaded later in GPU context."""
45
+ # HuggingFace token
46
  if self.config.HF_TOKEN:
47
  HfFolder.save_token(self.config.HF_TOKEN)
48
+ logging.info("HuggingFace token set")
49
 
50
+ # YOLO detection (CPU only)
51
  try:
52
+ self.models_cache['det'] = YOLO(self.config.YOLO_MODEL_PATH)
53
+ logging.info("✅ YOLO model loaded (CPU only)")
 
 
 
54
  except Exception as e:
55
+ logging.error(f"Failed to load YOLO model: {e}")
 
 
 
56
  raise
57
 
58
+ # Segmentation model (CPU)
59
  try:
60
+ self.models_cache['seg'] = load_model(self.config.SEG_MODEL_PATH, compile=False)
61
+ logging.info("✅ Segmentation model loaded (CPU)")
 
 
 
62
  except Exception as e:
63
  logging.warning(f"Segmentation model not available: {e}")
64
 
65
+ # Classification model (CPU)
66
  try:
67
  self.models_cache['cls'] = pipeline(
68
  'image-classification',
 
70
  token=self.config.HF_TOKEN,
71
  device='cpu'
72
  )
73
+ logging.info("✅ Classification pipeline loaded (CPU)")
74
  except Exception as e:
75
+ logging.warning(f"Classification pipeline not available: {e}")
76
 
77
+ # Embedding model (CPU)
78
  try:
79
  self.models_cache['embedding_model'] = HuggingFaceEmbeddings(
80
  model_name='sentence-transformers/all-MiniLM-L6-v2',
81
  model_kwargs={'device': 'cpu'}
82
  )
83
+ logging.info("✅ Embedding model loaded (CPU)")
84
  except Exception as e:
85
  logging.warning(f"Embedding model not available: {e}")
86
 
87
+ # Load PDF guidelines into FAISS
88
  self._load_knowledge_base()
89
 
90
  def _load_knowledge_base(self):
 
93
  if os.path.exists(pdf):
94
  loader = PyPDFLoader(pdf)
95
  docs.extend(loader.load())
96
+ logging.info(f"Loaded guideline PDF: {pdf}")
97
 
98
  if docs and 'embedding_model' in self.models_cache:
99
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
 
 
100
  chunks = splitter.split_documents(docs)
101
+ vs = FAISS.from_documents(chunks, self.models_cache['embedding_model'])
 
 
102
  self.knowledge_base_cache['vectorstore'] = vs
103
+ logging.info(f"✅ Knowledge base loaded with {len(chunks)} chunks")
104
  else:
105
  self.knowledge_base_cache['vectorstore'] = None
106
  logging.warning("Knowledge base unavailable")
107
 
108
  def perform_visual_analysis(self, image_pil):
109
+ """Detect & segment on CPU; return metrics and file paths."""
110
  if 'det' not in self.models_cache:
111
+ raise RuntimeError("YOLO model ('det') not loaded")
 
 
112
 
113
+ img_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
114
+ res = self.models_cache['det'].predict(img_cv, device='cpu', verbose=False)[0]
115
+ if not res.boxes:
116
+ raise ValueError("No wound detected")
 
 
 
 
 
117
 
118
+ x1, y1, x2, y2 = res.boxes.xyxy[0].cpu().numpy().astype(int)
119
+ region = img_cv[y1:y2, x1:x2]
 
120
 
121
+ # Save detection overlay
122
+ det_vis = img_cv.copy()
123
+ cv2.rectangle(det_vis, (x1, y1), (x2, y2), (0,255,0), 2)
124
+ os.makedirs(f"{self.config.UPLOADS_DIR}/analysis", exist_ok=True)
125
+ ts = datetime.now().strftime('%Y%m%d_%H%M%S')
126
+ det_path = f"{self.config.UPLOADS_DIR}/analysis/detection_{ts}.png"
127
+ cv2.imwrite(det_path, det_vis)
 
 
128
 
129
+ # Segmentation (if available)
130
+ length = breadth = area = 0
131
+ seg_path = None
132
+ if 'seg' in self.models_cache:
133
+ h, w = self.models_cache['seg'].input_shape[1:3]
134
+ inp = cv2.resize(region, (w,h)) / 255.0
135
+ mask = (self.models_cache['seg'].predict(np.expand_dims(inp,0))[0,:,:,0] > 0.5).astype(np.uint8)
136
+ mask_rs = cv2.resize(mask, (region.shape[1], region.shape[0]), interpolation=cv2.INTER_NEAREST)
137
+ ov = region.copy(); ov[mask_rs==1] = [0,0,255]
138
+ seg_vis = cv2.addWeighted(region,0.7,ov,0.3,0)
139
+ seg_path = f"{self.config.UPLOADS_DIR}/analysis/segmentation_{ts}.png"
140
+ cv2.imwrite(seg_path, seg_vis)
141
 
142
+ cnts, _ = cv2.findContours(mask_rs, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
143
+ if cnts:
144
+ cnt = max(cnts, key=cv2.contourArea)
145
+ _,_,w0,h0 = cv2.boundingRect(cnt)
146
+ length = round(h0/self.px_per_cm,2)
147
+ breadth= round(w0/self.px_per_cm,2)
148
+ area = round(cv2.contourArea(cnt)/(self.px_per_cm**2),2)
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
+ # Classification
151
+ wound_type = 'Unknown'
152
+ if 'cls' in self.models_cache:
153
+ try:
154
+ preds = self.models_cache['cls'](
155
+ Image.fromarray(cv2.cvtColor(region, cv2.COLOR_BGR2RGB))
156
  )
157
+ wound_type = max(preds, key=lambda x: x['score'])['label']
158
+ except Exception:
159
+ pass
 
 
 
 
 
160
 
161
+ return {
162
+ 'wound_type': wound_type,
163
+ 'length_cm': length,
164
+ 'breadth_cm': breadth,
165
+ 'surface_area_cm2': area,
166
+ 'detection_confidence': float(res.boxes.conf[0].cpu().item()),
167
+ 'detection_image_path': det_path,
168
+ 'segmentation_image_path': seg_path
169
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  def query_guidelines(self, query: str):
172
  vs = self.knowledge_base_cache.get('vectorstore')
 
174
  return "Clinical guidelines unavailable"
175
  docs = vs.as_retriever(search_kwargs={'k':10}).invoke(query)
176
  return '\n\n'.join(
177
+ f"Source: {d.metadata.get('source','?')}, Page: {d.metadata.get('page','?')}\n{d.page_content}"
 
178
  for d in docs
179
  )
180
 
181
  @spaces.GPU(enable_queue=True, duration=120)
182
+ def generate_final_report(self, patient_info, visual_results, guideline_context, image_pil, max_new_tokens=None):
 
 
183
  """Run MedGemma on GPU; return markdown report."""
184
  if 'medgemma_pipe' not in self.models_cache:
185
  try:
 
187
  'image-text-to-text',
188
  model='google/medgemma-4b-it',
189
  device='auto',
190
+ torch_dtype='auto',
191
  offload_folder='offload',
192
  token=self.config.HF_TOKEN
193
  )
194
  logging.info("✅ MedGemma pipeline loaded on GPU")
195
  except Exception as e:
196
+ logging.warning(f"MedGemma pipeline load failed: {e}")
197
+ return self._generate_fallback_report(patient_info, visual_results, guideline_context)
 
 
198
 
199
  msgs = [
200
  {'role':'system','content':[{'type':'text','text':default_system_prompt}]},
 
205
  for key in ('detection_image_path','segmentation_image_path'):
206
  p = visual_results.get(key)
207
  if p and os.path.exists(p):
208
+ msgs[1]['content'].append({'type':'image','image':Image.open(p)})
209
+ prompt = f"## Patient\n{patient_info}\n## Wound Type: {visual_results['wound_type']}"
 
 
 
 
 
210
  msgs[1]['content'].append({'type':'text','text':prompt})
211
 
212
  out = self.models_cache['medgemma_pipe'](
 
215
  do_sample=False
216
  )
217
  report = out[0]['generated_text'][-1].get('content','')
218
+ return report or self._generate_fallback_report(patient_info, visual_results, guideline_context)
 
 
219
 
220
  def _generate_fallback_report(self, patient_info, visual_results, guideline_context):
221
  dp = visual_results.get('detection_image_path','N/A')
222
  sp = visual_results.get('segmentation_image_path','N/A')
223
  return (
224
+ f"# Report\n{patient_info}\nType: {visual_results['wound_type']}\n"
225
+ f"Detection Image: {dp}\nSegmentation Image: {sp}\n"
 
 
226
  f"Guidelines: {guideline_context[:200]}..."
227
  )
228
 
 
231
  fn = f"{datetime.now():%Y%m%d_%H%M%S}.png"
232
  path = os.path.join(self.config.UPLOADS_DIR, fn)
233
  image_pil.convert('RGB').save(path)
234
+ if self.config.HF_TOKEN and getattr(self.config, 'DATASET_ID', None):
235
  try:
236
  api = HfApi()
237
  api.upload_file(
 
248
  try:
249
  saved = self.save_and_commit_image(image_pil)
250
  vis = self.perform_visual_analysis(image_pil)
251
+ info = ", ".join(f"{k}:{v}" for k, v in questionnaire_data.items() if v)
 
 
252
  gc = self.query_guidelines(info)
253
  report = self.generate_final_report(info, vis, gc, image_pil)
254
+ return {'success': True, 'visual_analysis': vis, 'report': report, 'saved_image_path': saved}
 
 
 
 
 
255
  except Exception as e:
256
  logging.error(f"Pipeline error: {e}")
257
  return {'success': False, 'error': str(e)}
 
262
  return self.full_analysis_pipeline(image, questionnaire_data)
263
 
264
  def _assess_risk_legacy(self, questionnaire_data):
265
+ """Legacy risk assessment for backward compatibility"""
266
+ risk_factors = []
267
+ risk_score = 0
268
+
269
  try:
270
+ # Age factor
271
  age = questionnaire_data.get('patient_age', 0)
272
  if age > 65:
273
+ risk_factors.append("Advanced age (>65)")
274
+ risk_score += 2
275
  elif age > 50:
276
+ risk_factors.append("Older adult (50-65)")
277
+ risk_score += 1
278
+
279
+ # Duration factor
280
+ duration = questionnaire_data.get('wound_duration', '').lower()
281
+ if any(term in duration for term in ['month', 'months', 'year']):
282
+ risk_factors.append("Chronic wound (>4 weeks)")
283
+ risk_score += 3
284
+
285
+ # Pain level
286
+ pain_level = questionnaire_data.get('pain_level', 0)
287
+ if pain_level >= 7:
288
+ risk_factors.append("High pain level")
289
+ risk_score += 2
290
+
291
+ # Medical history risk factors
292
+ medical_history = questionnaire_data.get('medical_history', '').lower()
293
+ if 'diabetes' in medical_history:
294
+ risk_factors.append("Diabetes mellitus")
295
+ risk_score += 3
296
+ if 'circulation' in medical_history or 'vascular' in medical_history:
297
+ risk_factors.append("Vascular/circulation issues")
298
+ risk_score += 2
299
+ if 'immune' in medical_history:
300
+ risk_factors.append("Immune system compromise")
301
+ risk_score += 2
302
+
303
+ # Determine risk level
304
+ if risk_score >= 7:
305
+ risk_level = "High"
306
+ elif risk_score >= 4:
307
+ risk_level = "Moderate"
308
+ else:
309
+ risk_level = "Low"
310
+
311
+ return {
312
+ 'risk_score': risk_score,
313
+ 'risk_level': risk_level,
314
+ 'risk_factors': risk_factors
315
+ }
316
+
317
  except Exception as e:
318
  logging.error(f"Risk assessment error: {e}")
319
+ return {'risk_score': 0, 'risk_level': 'Unknown', 'risk_factors': []}