EngReem85 commited on
Commit
f4bfc2c
·
verified ·
1 Parent(s): a9324fd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -71
app.py CHANGED
@@ -6,10 +6,18 @@ import tempfile
6
  import os
7
  import math
8
 
 
 
 
9
  mp_pose = mp.solutions.pose
10
  pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.7, model_complexity=1)
11
 
12
- def _dist(p1, p2): return math.hypot(p1[0]-p2[0], p1[1]-p2[1])
 
 
 
 
 
13
  def _angle(a, b, c):
14
  try:
15
  a, b, c = np.array(a), np.array(b), np.array(c)
@@ -23,9 +31,26 @@ def _angle(a, b, c):
23
  def _safe_mean(x): return float(np.mean(x)) if x else 0.0
24
  def _safe_std(x): return float(np.std(x)) if x else 0.0
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def analyze_gait(video_file):
27
  if video_file is None:
28
- return "<div>❌ يرجى رفع فيديو أولًا.</div>", None
29
 
30
  # حفظ الفيديو مؤقتًا
31
  if hasattr(video_file, "name"):
@@ -38,143 +63,231 @@ def analyze_gait(video_file):
38
 
39
  cap = cv2.VideoCapture(video_path)
40
  if not cap.isOpened():
41
- return "<div>❌ لا يمكن فتح الفيديو.</div>", None
42
 
43
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
44
  fps = float(cap.get(cv2.CAP_PROP_FPS) or 30.0)
45
  W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
46
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
 
 
47
  px2m = 1.7 / (H * 0.8)
48
 
49
- L_clear, R_clear, L_angle, R_angle, base_widths = [], [], [], [], []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  prev_L_ank, prev_R_ank = None, None
 
51
  frames_processed, person_detected = 0, False
52
- ground_y = H * 0.92
53
 
54
- while cap.isOpened() and frames_processed < min(1000, total_frames or 1000):
55
  ret, frame = cap.read()
56
- if not ret: break
 
57
  frames_processed += 1
 
58
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
59
  res = pose.process(frame_rgb)
60
- if not res.pose_landmarks: continue
 
61
 
62
  person_detected = True
63
  lm = res.pose_landmarks.landmark
64
- def xy(idx): return [lm[idx].x * W, lm[idx].y * H]
65
 
 
 
 
 
66
  L_ank = xy(mp_pose.PoseLandmark.LEFT_ANKLE.value)
67
  R_ank = xy(mp_pose.PoseLandmark.RIGHT_ANKLE.value)
68
  L_knee = xy(mp_pose.PoseLandmark.LEFT_KNEE.value)
69
  R_knee = xy(mp_pose.PoseLandmark.RIGHT_KNEE.value)
70
  L_foot = xy(mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value)
71
  R_foot = xy(mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value)
 
 
 
 
72
 
 
73
  Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100)
74
  Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100)
75
- L_clear.append(Lc)
76
- R_clear.append(Rc)
77
 
 
78
  La = _angle(L_knee, L_ank, L_foot)
79
  Ra = _angle(R_knee, R_ank, R_foot)
80
- L_angle.append(La)
81
- R_angle.append(Ra)
82
- base_widths.append(abs(L_ank[0]-R_ank[0]) * px2m)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  cap.release()
85
- try: os.unlink(video_path)
86
- except: pass
 
 
87
 
88
- if not person_detected:
89
- return "<div>❌ لم يتم اكتشاف شخص في الفيديو.</div>", None
90
 
91
- # ======== التحليل الإحصائي والديناميكي ========
 
 
92
  avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear)
93
  std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear)
94
  avg_La, avg_Ra = _safe_mean(L_angle), _safe_mean(R_angle)
95
- avg_base = _safe_mean(base_widths)
96
- diff_angle = abs(avg_La - avg_Ra)
97
- diff_clear = abs(avg_Lc - avg_Rc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  var_clear = max(std_Lc, std_Rc)
99
- min_clear = min(avg_Lc, avg_Rc)
100
-
101
- # نسب زمنية لأنماط غير طبيعية
102
- L_low_ratio = sum(np.array(L_clear) < 4) / len(L_clear) if L_clear else 0
103
- R_low_ratio = sum(np.array(R_clear) < 4) / len(R_clear) if R_clear else 0
104
- dynamic_asym = np.mean(np.abs(np.array(L_clear) - np.array(R_clear))) if L_clear and R_clear else 0
105
-
106
- # ======== خوارزمية التصنيف متعددة المعايير ========
107
- score = 0
108
-
109
- # حساسية أعلى للحالات العصبية
110
- if (L_low_ratio > 0.4 or R_low_ratio > 0.4) and min_clear < 3.5:
111
- score += 3.0 # Foot drop قوي
112
-
113
- if diff_angle > 20 and var_clear > 9:
114
- score += 2.5 # Neuropathy قوي
115
 
116
- if avg_base > 0.27 and var_clear > 7 and diff_angle > 10:
117
- score += 2.5 # Charcot قوي
 
 
 
 
 
 
 
 
118
 
119
- if dynamic_asym > 5 and var_clear > 8:
 
120
  score += 1.5
121
 
122
- if diff_clear > 6:
 
123
  score += 1.0
124
 
125
- if std_Lc > 10 or std_Rc > 10:
126
- score += 0.5
127
-
128
- # تجاهل المشي الطليعي المنتظم
129
- if avg_Lc > 10 and avg_Rc > 10 and var_clear < 5:
130
- return (
131
- "<div style='color:#2e7d32;font-weight:600'>✅ المشية طليعية طبيعية.</div>"
132
- "<div>تم التعرف على مشية طليعية منتظمة دون مؤشرات لخلل عصبي.</div>"
133
- "<div style='font-size:13px;color:#555;margin-top:8px'>⚠️ يُوصى بالمراجعة فقط في حال وجود ألم أو ضعف توازن.</div>",
134
- 0.1
135
  )
136
-
137
- # ======== تحليل الحالة النهائية ========
138
- max_score = 9.0
 
 
 
 
 
139
  norm_score = min(score / max_score, 1.0)
140
 
141
- if norm_score < 0.3:
142
- level, color, desc = "🟢 طبيعي", "#2e7d32", "المشية ضمن النطاق الطبيعي."
143
- elif norm_score < 0.55:
144
- level, color, desc = "🟡 متوسطة الخطورة", "#fbc02d", "بعض الاختلافات الطفيفة."
 
145
  else:
146
- level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات قوية لخلل في المشية."
147
 
148
- # تحديد الحالة المحتملة
149
- if score >= 6 and avg_base > 0.27:
150
  condition = "قدم شاركوت (Charcot Foot)"
151
- elif (L_low_ratio > 0.4 or R_low_ratio > 0.4):
152
  condition = "ضعف العضلة الظنبوبية (Foot Drop)"
153
- elif diff_angle > 20 and var_clear > 9:
154
  condition = "اعتلال الأعصاب المحيطية / السكري"
155
  else:
156
  condition = "غير محددة بدقة"
157
 
158
  html = f"""
159
  <div style='color:{color};font-weight:700;font-size:18px'>{level}</div>
 
160
  <div>🩺 الحالة المحتملة: <b>{condition}</b></div>
161
- <div>📊 درجة الخطورة: <b>{score:.1f}/9</b></div>
162
  <div>{desc}</div>
163
- <div style='font-size:13px;color:#555;margin-top:8px'>⚠️ التحليل يعتمد على الأنماط الديناميكية في الفيديو ولا يُغني عن الفحص الطبي.</div>
164
  """
165
- return html, norm_score
166
 
 
167
 
168
- with gr.Blocks(title="تحليل المشية العصبية - الإصدار الخامس") as demo:
169
- gr.Markdown("## 🩺 نظام تحليل المشية العصبية (v5) – توازن بين الحساسية والدقة")
170
- gr.Markdown("ارفع فيديو جانبي للمشي (15–30 ثانية). النظام سيقيّم النمط العصبي بدقة عالية.")
 
 
 
171
 
172
  with gr.Row():
173
  with gr.Column(scale=1):
174
  video_in = gr.File(label="📂 اختر فيديو المشي", file_types=[".mp4", ".avi", ".mov"], type="binary")
175
  analyze_btn = gr.Button("🔍 بدء التحليل", variant="primary")
176
  with gr.Column(scale=1):
177
- gauge = gr.Number(label="⚙️ درجة الخطورة (0–1)", interactive=False)
178
  out_html = gr.HTML("<i>النتيجة ستظهر هنا...</i>")
179
 
180
  analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])
 
6
  import os
7
  import math
8
 
9
+ # ===========================
10
+ # إعداد Mediapipe Pose
11
+ # ===========================
12
  mp_pose = mp.solutions.pose
13
  pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.7, model_complexity=1)
14
 
15
+ # ===========================
16
+ # دوال مساعدة
17
+ # ===========================
18
+ def _dist(p1, p2):
19
+ return math.hypot(p1[0]-p2[0], p1[1]-p2[1])
20
+
21
  def _angle(a, b, c):
22
  try:
23
  a, b, c = np.array(a), np.array(b), np.array(c)
 
31
  def _safe_mean(x): return float(np.mean(x)) if x else 0.0
32
  def _safe_std(x): return float(np.std(x)) if x else 0.0
33
 
34
+ def _gauge_html(norm_score):
35
+ # norm_score بين 0 و 1
36
+ pct = int(max(0, min(1, norm_score)) * 100)
37
+ # تدرّج أخضر→أصفر→أحمر
38
+ bar = f"""
39
+ <div style="width:100%;background:#eee;border-radius:10px;height:16px;overflow:hidden;border:1px solid #ddd;">
40
+ <div style="width:{pct}%;height:100%;
41
+ background: linear-gradient(90deg,#4caf50,#ffeb3b,#f44336);
42
+ filter: saturate(1.2);"></div>
43
+ </div>
44
+ <div style="font-size:12px;color:#555;margin-top:6px">Risk: {pct}%</div>
45
+ """
46
+ return bar
47
+
48
+ # ===========================
49
+ # التحليل الرئيسي للفيديو
50
+ # ===========================
51
  def analyze_gait(video_file):
52
  if video_file is None:
53
+ return "<div>❌ يرجى رفع فيديو أولًا.</div>", "<div></div>"
54
 
55
  # حفظ الفيديو مؤقتًا
56
  if hasattr(video_file, "name"):
 
63
 
64
  cap = cv2.VideoCapture(video_path)
65
  if not cap.isOpened():
66
+ return "<div>❌ لا يمكن فتح الفيديو.</div>", "<div></div>"
67
 
68
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
69
  fps = float(cap.get(cv2.CAP_PROP_FPS) or 30.0)
70
  W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
71
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
72
+
73
+ # تقدير تحويل بيكسل→متر (تقريبي حسب الطول 1.7م و 80% من الارتفاع إطار)
74
  px2m = 1.7 / (H * 0.8)
75
 
76
+ # قوائم القياسات عبر الإطارات
77
+ L_clear, R_clear = [], []
78
+ L_angle, R_angle = [], []
79
+ base_px_seq, base_m_seq = [], []
80
+ torso_tilt_seq = [] # ميل الجذع (درجات) بالنسبة للمحور الرأسي
81
+ step_L, step_R = [], []
82
+
83
+ # كواشف زمنية (نِسَب)
84
+ low_clear_flags_L, low_clear_flags_R = 0, 0
85
+ asym_clear_flags = 0
86
+ lean_flags = 0
87
+
88
+ # مرجعية أرض
89
+ ground_y = H * 0.92
90
+
91
+ # لحساب طول الخطوة تقريبياً
92
  prev_L_ank, prev_R_ank = None, None
93
+
94
  frames_processed, person_detected = 0, False
 
95
 
96
+ while cap.isOpened() and frames_processed < min(1200, total_frames or 1200):
97
  ret, frame = cap.read()
98
+ if not ret:
99
+ break
100
  frames_processed += 1
101
+
102
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
103
  res = pose.process(frame_rgb)
104
+ if not res.pose_landmarks:
105
+ continue
106
 
107
  person_detected = True
108
  lm = res.pose_landmarks.landmark
 
109
 
110
+ def xy(idx):
111
+ return [lm[idx].x * W, lm[idx].y * H]
112
+
113
+ # نقاط مهمة
114
  L_ank = xy(mp_pose.PoseLandmark.LEFT_ANKLE.value)
115
  R_ank = xy(mp_pose.PoseLandmark.RIGHT_ANKLE.value)
116
  L_knee = xy(mp_pose.PoseLandmark.LEFT_KNEE.value)
117
  R_knee = xy(mp_pose.PoseLandmark.RIGHT_KNEE.value)
118
  L_foot = xy(mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value)
119
  R_foot = xy(mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value)
120
+ L_hip = xy(mp_pose.PoseLandmark.LEFT_HIP.value)
121
+ R_hip = xy(mp_pose.PoseLandmark.RIGHT_HIP.value)
122
+ L_sh = xy(mp_pose.PoseLandmark.LEFT_SHOULDER.value)
123
+ R_sh = xy(mp_pose.PoseLandmark.RIGHT_SHOULDER.value)
124
 
125
+ # ارتفاع القدم بالسنتيمتر (أقرب نقطة من الكاحل/مقدمة القدم للأرض)
126
  Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100)
127
  Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100)
128
+ L_clear.append(Lc); R_clear.append(Rc)
 
129
 
130
+ # زوايا الكاحل
131
  La = _angle(L_knee, L_ank, L_foot)
132
  Ra = _angle(R_knee, R_ank, R_foot)
133
+ L_angle.append(La); R_angle.append(Ra)
134
+
135
+ # مسافة تباعد الكاحلين (بالبكسل والمتر)
136
+ base_px = abs(L_ank[0] - R_ank[0])
137
+ base_px_seq.append(base_px)
138
+ base_m_seq.append(base_px * px2m)
139
+
140
+ # ميل الجذع: زاوية (منتصف الكتفين → منتصف الوركين) مقابل الرأسي
141
+ mid_sh = [(L_sh[0]+R_sh[0])/2, (L_sh[1]+R_sh[1])/2]
142
+ mid_hip= [(L_hip[0]+R_hip[0])/2, (L_hip[1]+R_hip[1])/2]
143
+ vec = np.array([mid_sh[0]-mid_hip[0], mid_sh[1]-mid_hip[1]]) # من الحوض للأكتاف
144
+ # زاوية مع المحور الرأسي (0 = عمودي مثالي)
145
+ tilt = abs(90 - abs(math.degrees(math.atan2(abs(vec[1]), abs(vec[0])+1e-6))))
146
+ torso_tilt_seq.append(tilt)
147
+
148
+ # تقدير طول الخطوة (تقريبي) من حركة الكاحل إطارياً
149
+ if prev_L_ank is not None:
150
+ step_L.append(_dist(L_ank, prev_L_ank) * px2m)
151
+ if prev_R_ank is not None:
152
+ step_R.append(_dist(R_ank, prev_R_ank) * px2m)
153
+ prev_L_ank, prev_R_ank = L_ank, R_ank
154
+
155
+ # كواشف زمنية لكل إطار:
156
+ if Lc < 3.5: low_clear_flags_L += 1
157
+ if Rc < 3.5: low_clear_flags_R += 1
158
+ if abs(Lc - Rc) > 6.0: asym_clear_flags += 1
159
+ if tilt > 8.0: lean_flags += 1
160
 
161
  cap.release()
162
+ try:
163
+ os.unlink(video_path)
164
+ except:
165
+ pass
166
 
167
+ if not person_detected or frames_processed == 0:
168
+ return "<div>❌ لم يتم اكتشاف شخص في الفيديو. يُفضّل تصوير جانبي أو أمامي واضح وإضاءة جيدة.</div>", "<div></div>"
169
 
170
+ # ===========================
171
+ # إحصاءات أساسية
172
+ # ===========================
173
  avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear)
174
  std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear)
175
  avg_La, avg_Ra = _safe_mean(L_angle), _safe_mean(R_angle)
176
+ avg_base_m = _safe_mean(base_m_seq)
177
+ avg_tilt = _safe_mean(torso_tilt_seq)
178
+
179
+ # تقدير العرض النسبي لتحديد زاوية التصوير (أمامي/جانبي)
180
+ # إذا كانت المسافة الأفقية بين الكاحلين نسبةً إلى عرض الإطار كبيرة → غالبًا أمامي
181
+ view_ratio = _safe_mean(base_px_seq) / max(1, W)
182
+ view = "frontal" if view_ratio > 0.18 else "side"
183
+
184
+ # نسب زمنية (قوة دليل عبر الزمن)
185
+ n = max(1, len(L_clear))
186
+ ratio_low_L = low_clear_flags_L / n
187
+ ratio_low_R = low_clear_flags_R / n
188
+ ratio_asym = asym_clear_flags / n
189
+ ratio_lean = lean_flags / n
190
+
191
+ # ===========================
192
+ # منطق متعدد الشروط (متوازن)
193
+ # ===========================
194
+ score = 0.0
195
+ strong_votes = 0
196
+
197
+ # 1) Foot Drop-like: انخفاض مستمر + نسب زمنية عالية
198
+ foot_drop_evidence = (min(avg_Lc, avg_Rc) < 3.5) or (ratio_low_L > 0.35 or ratio_low_R > 0.35)
199
+ if view == "side":
200
+ if foot_drop_evidence and abs(avg_La - avg_Ra) < 35:
201
+ score += 3.0; strong_votes += 1
202
+ elif (ratio_low_L > 0.2 or ratio_low_R > 0.2):
203
+ score += 1.5
204
+
205
+ # 2) Neuropathy-like: تذبذب ارتفاع واضح + لا تماثل زاوٍي
206
  var_clear = max(std_Lc, std_Rc)
207
+ diff_angle = abs(avg_La - avg_Ra)
208
+ if (var_clear > 9 and diff_angle > 16) or (ratio_asym > 0.25 and var_clear > 7):
209
+ score += 2.5; strong_votes += 1
210
+ elif (var_clear > 7 and diff_angle > 12):
211
+ score += 1.5
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ # 3) Charcot-like: قاعدة عريضة + ميل جذع/عدم اتزان (أكثر منطقية في الأمامي)
214
+ if view == "frontal":
215
+ if (avg_base_m > 0.28 and ratio_lean > 0.25) or (avg_base_m > 0.30):
216
+ score += 3.0; strong_votes += 1
217
+ elif avg_base_m > 0.25 and ratio_lean > 0.15:
218
+ score += 1.5
219
+ else:
220
+ # من الجانب: نعتمد أكثر على عدم التماثل الزمني
221
+ if ratio_asym > 0.35 and var_clear > 8:
222
+ score += 2.5; strong_votes += 1
223
 
224
+ # 4) ميل الجذع المستمر (واضح سريريًا)
225
+ if ratio_lean > 0.35 or avg_tilt > 10:
226
  score += 1.5
227
 
228
+ # 5) اختلاف الارتفاع بين القدمين (متوسط)
229
+ if abs(avg_Lc - avg_Rc) > 6:
230
  score += 1.0
231
 
232
+ # 6) فلاتر السماح بالمشي الطليعي المنتظم (Toe-walking) دون تصنيف مرضي
233
+ if (avg_Lc > 10 and avg_Rc > 10) and (var_clear < 5) and (ratio_asym < 0.15) and (ratio_lean < 0.15):
234
+ html = (
235
+ "<div style='color:#2e7d32;font-weight:600'>✅ المشية طليعية منتظمة.</div>"
236
+ "<div>تم التعرف على نمط مشي طليعي مستقر دون مؤشرات عصبية مقلقة.</div>"
237
+ "<div style='font-size:13px;color:#555;margin-top:8px'>⚠️ يُوصى بالمراجعة فقط إذا وُجد ألم أو عدم اتزان.</div>"
 
 
 
 
238
  )
239
+ return html, _gauge_html(0.1)
240
+
241
+ # ===========================
242
+ # تسوية متوازنة للحكم النهائي
243
+ # - إذا وُجدت مؤشرين قويين على الأقل → نرفع التصنيف
244
+ # - وإلا نستخدم الدرجة المعيارية
245
+ # ===========================
246
+ max_score = 10.0
247
  norm_score = min(score / max_score, 1.0)
248
 
249
+ # عتبات متوازنة (لا مبالغة في التشدد ولا التساهل)
250
+ if (strong_votes >= 2) or (norm_score >= 0.7):
251
+ level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات قوية ومتعددة لخلل في المشية."
252
+ elif norm_score >= 0.45:
253
+ level, color, desc = "🟡 متوسطة الخطورة", "#f9a825", "تم رصد مؤشرات ملحوظة تحتاج متابعة."
254
  else:
255
+ level, color, desc = "🟢 طبيعي", "#2e7d32", "المشية ضمن الحدود الطبيعية."
256
 
257
+ # ترجيح نوع الحالة
258
+ if (view == "frontal" and avg_base_m > 0.28 and ratio_lean > 0.25) or (avg_base_m > 0.30):
259
  condition = "قدم شاركوت (Charcot Foot)"
260
+ elif foot_drop_evidence and view == "side":
261
  condition = "ضعف العضلة الظنبوبية (Foot Drop)"
262
+ elif (var_clear > 9 and diff_angle > 16) or (ratio_asym > 0.25 and var_clear > 7):
263
  condition = "اعتلال الأعصاب المحيطية / السكري"
264
  else:
265
  condition = "غير محددة بدقة"
266
 
267
  html = f"""
268
  <div style='color:{color};font-weight:700;font-size:18px'>{level}</div>
269
+ <div>👁️ زاوية التصوير المُكتشفة: <b>{'أمامية' if view=='frontal' else 'جانبية'}</b></div>
270
  <div>🩺 الحالة المحتملة: <b>{condition}</b></div>
271
+ <div>📊 درجة المؤشرات: <b>{score:.1f}/{max_score:.0f}</b></div>
272
  <div>{desc}</div>
273
+ <div style='font-size:13px;color:#555;margin-top:8px'>⚠️ تحليل تقديري يعتمد على أنماط الحركة؛ لا يُغني عن التقييم الطبي.</div>
274
  """
 
275
 
276
+ return html, _gauge_html(norm_score)
277
 
278
+ # ===========================
279
+ # واجهة Gradio
280
+ # ===========================
281
+ with gr.Blocks(title="تحليل المشية العصبية - v6 (متوازن مع كشف الزاوية)") as demo:
282
+ gr.Markdown("## 🩺 تحليل المشية العصبية – الإصدار v6")
283
+ gr.Markdown("يرجى رفع فيديو جانبي أو أمامي (15–30 ثانية). النظام يحدد زاوية التصوير تلقائيًا ويعرض تقييمًا متوازنًا مع شريط ألوان.")
284
 
285
  with gr.Row():
286
  with gr.Column(scale=1):
287
  video_in = gr.File(label="📂 اختر فيديو المشي", file_types=[".mp4", ".avi", ".mov"], type="binary")
288
  analyze_btn = gr.Button("🔍 بدء التحليل", variant="primary")
289
  with gr.Column(scale=1):
290
+ gauge = gr.HTML("<div></div>")
291
  out_html = gr.HTML("<i>النتيجة ستظهر هنا...</i>")
292
 
293
  analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])