Okidi Norbert commited on
Commit
45dcdd6
Β·
1 Parent(s): 391c939

Final production push with CORS, model, and video fixes

Browse files
.gitattributes ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ *.png filter=xet diff=xet merge=xet
2
+ *.jpg filter=xet diff=xet merge=xet
3
+ *.jpeg filter=xet diff=xet merge=xet
4
+ *.gif filter=xet diff=xet merge=xet
5
+ *.pt filter=xet diff=xet merge=xet
6
+ *.pth filter=xet diff=xet merge=xet
7
+ *.weights filter=xet diff=xet merge=xet
DEPLOYMENT.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BAKO-AI Deployment Guide
2
+
3
+ This guide explains how to manage and deploy the BAKO-AI analysis pipeline to Hugging Face.
4
+
5
+ ## πŸ— Architecture Overview
6
+ - **Application**: Hosted on Hugging Face Spaces (`icanedit2/BakoAI`).
7
+ - **Models**: Hosted on a dedicated Hugging Face Model Repository (`icanedit2/bakoai-models`).
8
+ - **Storage/DB**: Managed via Supabase.
9
+
10
+ ## πŸš€ Deployment Workflow
11
+
12
+ ### 1. Push Code Changes
13
+ To push local changes from the `back-end` folder to the Hugging Face Space root:
14
+ ```bash
15
+ # From the root directory:
16
+ git subtree split --prefix back-end -b hf-production-build
17
+ git push hf hf-production-build:main --force
18
+ git branch -D hf-production-build
19
+ ```
20
+
21
+ ### 🧠 2. Managing Models
22
+ Models are stored in the `icanedit2/bakoai-models` repository to prevent `git push` from deleting them.
23
+ - To add a new model, upload it to the `bakoai-models` repo.
24
+ - Update `download_models.py` in the Space repo to include the new filename.
25
+ - The build process will automatically download models into `/home/user/app/models/`.
26
+
27
+ ### πŸ” 3. Security (Hugging Face Secrets)
28
+ Ensure the following Secrets are set in your Space Settings:
29
+ - `HF_TOKEN`: Required for downloading models from private repositories.
30
+ - `SUPABASE_URL`: Your Supabase project URL.
31
+ - `SUPABASE_KEY`: Your Supabase anonymous/service key.
32
+ - `JWT_SECRET`: For authentication.
33
+
34
+ ## 🎬 Video Playback Optimization
35
+ All annotated videos are processed through FFmpeg with the following settings for maximum browser compatibility:
36
+ - **Codec**: libx264
37
+ - **Pixel Format**: yuv420p
38
+ - **Dimensions**: Forced to even values (H.264 requirement)
39
+ - **Streaming**: `+faststart` enabled for instant playback.
app/api/admin.py CHANGED
@@ -59,7 +59,7 @@ async def update_organization(
59
  async def update_user_role(
60
  user_id: str,
61
  role_data: dict,
62
- current_user: dict = Depends(require_team_account),
63
  supabase: SupabaseService = Depends(get_supabase),
64
  ):
65
  """
@@ -83,7 +83,7 @@ async def update_user_role(
83
  @router.get("/users")
84
  async def get_users(
85
  role: Optional[str] = None,
86
- current_user: dict = Depends(require_team_account),
87
  supabase: SupabaseService = Depends(get_supabase),
88
  ):
89
  """
@@ -146,7 +146,7 @@ async def get_users(
146
  @router.post("/players")
147
  async def create_player(
148
  player_data: PlayerCreate,
149
- current_user: dict = Depends(require_team_account),
150
  supabase: SupabaseService = Depends(get_supabase),
151
  ):
152
  """
@@ -177,7 +177,7 @@ async def create_player(
177
  async def update_player(
178
  player_id: str,
179
  player_data: PlayerUpdate,
180
- current_user: dict = Depends(require_team_account),
181
  supabase: SupabaseService = Depends(get_supabase),
182
  ):
183
  """
@@ -210,7 +210,7 @@ async def update_player(
210
 
211
  @router.get("/players")
212
  async def get_roster(
213
- current_user: dict = Depends(require_team_account),
214
  supabase: SupabaseService = Depends(get_supabase),
215
  ):
216
  """
@@ -222,7 +222,7 @@ async def get_roster(
222
  async def update_player_status(
223
  player_id: str,
224
  status_data: dict,
225
- current_user: dict = Depends(require_team_account),
226
  supabase: SupabaseService = Depends(get_supabase),
227
  ):
228
  """
@@ -253,7 +253,7 @@ async def update_player_status(
253
  @router.delete("/players/{player_id}")
254
  async def delete_player(
255
  player_id: str,
256
- current_user: dict = Depends(require_team_account),
257
  supabase: SupabaseService = Depends(get_supabase),
258
  ):
259
  """
@@ -294,7 +294,7 @@ async def delete_player(
294
  async def link_player_account(
295
  player_id: str,
296
  link_data: dict,
297
- current_user: dict = Depends(require_team_account),
298
  supabase: SupabaseService = Depends(get_supabase),
299
  ):
300
  """
@@ -438,7 +438,7 @@ async def link_player_account(
438
 
439
  @router.get("/staff")
440
  async def get_staff(
441
- current_user: dict = Depends(require_team_account),
442
  supabase: SupabaseService = Depends(get_supabase),
443
  ):
444
  """
@@ -462,8 +462,8 @@ async def get_staff(
462
  @router.post("/staff-link")
463
  @router.post("/staff-link/")
464
  async def link_staff_member(
465
- link_data: dict, # email, role
466
- current_user: dict = Depends(require_organization_admin),
467
  supabase: SupabaseService = Depends(get_supabase),
468
  ):
469
  """
@@ -583,7 +583,7 @@ async def remove_staff_member(
583
 
584
  @router.get("/schedule")
585
  async def get_schedule(
586
- current_user: dict = Depends(require_team_account),
587
  supabase: SupabaseService = Depends(get_supabase),
588
  ):
589
  """Get team schedule."""
@@ -663,7 +663,7 @@ async def delete_schedule_event(
663
 
664
  @router.get("/matches")
665
  async def get_matches(
666
- current_user: dict = Depends(require_team_account),
667
  supabase: SupabaseService = Depends(get_supabase),
668
  ):
669
  """Get matches for the current user's organization (Owner or Staff)."""
@@ -733,7 +733,7 @@ async def delete_match(
733
  @router.get("/matches/{match_id}/player-stats")
734
  async def get_match_player_stats(
735
  match_id: str,
736
- current_user: dict = Depends(require_team_account),
737
  supabase: SupabaseService = Depends(get_supabase),
738
  ):
739
  """Get all player stats for a specific match, enriched with player name/jersey info."""
@@ -795,7 +795,7 @@ async def get_notifications(
795
  @router.post("/notifications")
796
  async def create_notification(
797
  notif_data: NotificationCreate,
798
- current_user: dict = Depends(require_team_account),
799
  supabase: SupabaseService = Depends(get_supabase),
800
  ):
801
  notif_dict = notif_data.model_dump()
@@ -828,7 +828,7 @@ async def mark_all_notifications_read(
828
  async def update_notification(
829
  notification_id: str,
830
  notification_data: NotificationCreate,
831
- current_user: dict = Depends(require_team_account),
832
  supabase: SupabaseService = Depends(get_supabase),
833
  ):
834
  update_dict = notification_data.model_dump(exclude_unset=True)
@@ -847,7 +847,7 @@ async def delete_notification(
847
 
848
  @router.get("/stats")
849
  async def get_stats(
850
- current_user: dict = Depends(require_team_account),
851
  supabase: SupabaseService = Depends(get_supabase),
852
  ):
853
  # Mock aggregation or real counts
@@ -894,7 +894,7 @@ async def get_stats(
894
 
895
  @router.get("/profile")
896
  async def get_profile(
897
- current_user: dict = Depends(require_team_account),
898
  supabase: SupabaseService = Depends(get_supabase),
899
  ):
900
  # Get user info and associated organization
@@ -915,7 +915,7 @@ async def get_profile(
915
  @router.put("/profile")
916
  async def update_profile(
917
  profile_data: dict,
918
- current_user: dict = Depends(require_team_account),
919
  supabase: SupabaseService = Depends(get_supabase),
920
  ):
921
  # Update user or org
 
59
  async def update_user_role(
60
  user_id: str,
61
  role_data: dict,
62
+ current_user: dict = Depends(require_staff_member),
63
  supabase: SupabaseService = Depends(get_supabase),
64
  ):
65
  """
 
83
  @router.get("/users")
84
  async def get_users(
85
  role: Optional[str] = None,
86
+ current_user: dict = Depends(require_staff_member),
87
  supabase: SupabaseService = Depends(get_supabase),
88
  ):
89
  """
 
146
  @router.post("/players")
147
  async def create_player(
148
  player_data: PlayerCreate,
149
+ current_user: dict = Depends(require_staff_member),
150
  supabase: SupabaseService = Depends(get_supabase),
151
  ):
152
  """
 
177
  async def update_player(
178
  player_id: str,
179
  player_data: PlayerUpdate,
180
+ current_user: dict = Depends(require_staff_member),
181
  supabase: SupabaseService = Depends(get_supabase),
182
  ):
183
  """
 
210
 
211
  @router.get("/players")
212
  async def get_roster(
213
+ current_user: dict = Depends(require_staff_member),
214
  supabase: SupabaseService = Depends(get_supabase),
215
  ):
216
  """
 
222
  async def update_player_status(
223
  player_id: str,
224
  status_data: dict,
225
+ current_user: dict = Depends(require_staff_member),
226
  supabase: SupabaseService = Depends(get_supabase),
227
  ):
228
  """
 
253
  @router.delete("/players/{player_id}")
254
  async def delete_player(
255
  player_id: str,
256
+ current_user: dict = Depends(require_staff_member),
257
  supabase: SupabaseService = Depends(get_supabase),
258
  ):
259
  """
 
294
  async def link_player_account(
295
  player_id: str,
296
  link_data: dict,
297
+ current_user: dict = Depends(require_staff_member),
298
  supabase: SupabaseService = Depends(get_supabase),
299
  ):
300
  """
 
438
 
439
  @router.get("/staff")
440
  async def get_staff(
441
+ current_user: dict = Depends(require_staff_member),
442
  supabase: SupabaseService = Depends(get_supabase),
443
  ):
444
  """
 
462
  @router.post("/staff-link")
463
  @router.post("/staff-link/")
464
  async def link_staff_member(
465
+ link_data: StaffLinkRequest,
466
+ current_user: dict = Depends(require_team_account),
467
  supabase: SupabaseService = Depends(get_supabase),
468
  ):
469
  """
 
583
 
584
  @router.get("/schedule")
585
  async def get_schedule(
586
+ current_user: dict = Depends(require_staff_member),
587
  supabase: SupabaseService = Depends(get_supabase),
588
  ):
589
  """Get team schedule."""
 
663
 
664
  @router.get("/matches")
665
  async def get_matches(
666
+ current_user: dict = Depends(require_staff_member),
667
  supabase: SupabaseService = Depends(get_supabase),
668
  ):
669
  """Get matches for the current user's organization (Owner or Staff)."""
 
733
  @router.get("/matches/{match_id}/player-stats")
734
  async def get_match_player_stats(
735
  match_id: str,
736
+ current_user: dict = Depends(require_staff_member),
737
  supabase: SupabaseService = Depends(get_supabase),
738
  ):
739
  """Get all player stats for a specific match, enriched with player name/jersey info."""
 
795
  @router.post("/notifications")
796
  async def create_notification(
797
  notif_data: NotificationCreate,
798
+ current_user: dict = Depends(require_staff_member),
799
  supabase: SupabaseService = Depends(get_supabase),
800
  ):
801
  notif_dict = notif_data.model_dump()
 
828
  async def update_notification(
829
  notification_id: str,
830
  notification_data: NotificationCreate,
831
+ current_user: dict = Depends(require_staff_member),
832
  supabase: SupabaseService = Depends(get_supabase),
833
  ):
834
  update_dict = notification_data.model_dump(exclude_unset=True)
 
847
 
848
  @router.get("/stats")
849
  async def get_stats(
850
+ current_user: dict = Depends(require_staff_member),
851
  supabase: SupabaseService = Depends(get_supabase),
852
  ):
853
  # Mock aggregation or real counts
 
894
 
895
  @router.get("/profile")
896
  async def get_profile(
897
+ current_user: dict = Depends(require_staff_member),
898
  supabase: SupabaseService = Depends(get_supabase),
899
  ):
900
  # Get user info and associated organization
 
915
  @router.put("/profile")
916
  async def update_profile(
917
  profile_data: dict,
918
+ current_user: dict = Depends(require_staff_member),
919
  supabase: SupabaseService = Depends(get_supabase),
920
  ):
921
  # Update user or org
app/dependencies.py CHANGED
@@ -192,7 +192,7 @@ async def require_linked_account(
192
 
193
 
194
  async def require_staff_member(
195
- current_user: dict = Depends(require_team_account),
196
  ) -> dict:
197
  """
198
  Dependency that requires the user to be a COACH or TEAM owner who is linked to an organization.
 
192
 
193
 
194
  async def require_staff_member(
195
+ current_user: dict = Depends(get_current_user_with_db),
196
  ) -> dict:
197
  """
198
  Dependency that requires the user to be a COACH or TEAM owner who is linked to an organization.
app/main.py CHANGED
@@ -54,6 +54,26 @@ async def lifespan(app: FastAPI):
54
  # Ensure upload directory exists
55
  os.makedirs(settings.upload_dir, exist_ok=True)
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  # Check GPU availability
58
  if settings.gpu_enabled:
59
  try:
@@ -106,12 +126,11 @@ def create_app() -> FastAPI:
106
  app.add_middleware(SlowAPIMiddleware)
107
 
108
  # Configure CORS
109
- # - In production, require explicit origins (settings.cors_origins)
110
- # - In debug, allow "*" but disable credentials to avoid insecure/invalid combo
111
  cors_origins = settings.cors_origins_list
112
- if settings.debug and not cors_origins:
 
113
  allow_origins = ["*"]
114
- allow_credentials = False
115
  else:
116
  allow_origins = cors_origins
117
  allow_credentials = True
 
54
  # Ensure upload directory exists
55
  os.makedirs(settings.upload_dir, exist_ok=True)
56
 
57
+ # Check for required models
58
+ required_models = [
59
+ settings.player_detector_path,
60
+ settings.ball_detector_path,
61
+ settings.court_keypoint_detector_path,
62
+ settings.swish_ball_rim_model,
63
+ settings.swish_pose_model
64
+ ]
65
+
66
+ missing_models = []
67
+ for model_path in required_models:
68
+ if not os.path.exists(model_path):
69
+ missing_models.append(model_path)
70
+
71
+ if missing_models:
72
+ print(f"❌ CRITICAL: Missing models: {', '.join(missing_models)}")
73
+ print(" Please run 'python download_models.py' to fetch them.")
74
+ else:
75
+ print("βœ… All required models are present.")
76
+
77
  # Check GPU availability
78
  if settings.gpu_enabled:
79
  try:
 
126
  app.add_middleware(SlowAPIMiddleware)
127
 
128
  # Configure CORS
 
 
129
  cors_origins = settings.cors_origins_list
130
+ if not cors_origins:
131
+ # Permissive for development/unconfigured production
132
  allow_origins = ["*"]
133
+ allow_credentials = False # Cannot use credentials with "*"
134
  else:
135
  allow_origins = cors_origins
136
  allow_credentials = True
debug_path.py CHANGED
@@ -10,7 +10,20 @@ def _resolve_model_path(path: str) -> str:
10
  """Safely resolve model path relative to backend root."""
11
  if os.path.isabs(path):
12
  return path
13
- return os.path.abspath(os.path.join(_BACKEND_ROOT, path))
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  res_rim = _resolve_model_path(settings.swish_ball_rim_model)
16
  res_pose = _resolve_model_path(settings.swish_pose_model)
 
10
  """Safely resolve model path relative to backend root."""
11
  if os.path.isabs(path):
12
  return path
13
+
14
+ # Anchor to the backend root (directory containing this script)
15
+ candidate = os.path.abspath(os.path.join(_BACKEND_ROOT, path))
16
+
17
+ if os.path.exists(candidate):
18
+ return candidate
19
+
20
+ # Robust fallback for 'DON'T TOUCH' mangled environments
21
+ if "OKIDI-DONT TOUCH" in candidate:
22
+ healed = candidate.replace("OKIDI-DONT TOUCH", "OKIDI-DON'T TOUCH")
23
+ if os.path.exists(healed):
24
+ return healed
25
+
26
+ return candidate
27
 
28
  res_rim = _resolve_model_path(settings.swish_ball_rim_model)
29
  res_pose = _resolve_model_path(settings.swish_pose_model)
download_models.py CHANGED
@@ -1,49 +1,41 @@
1
  import os
2
- import requests
3
- from tqdm import tqdm
4
 
5
- def download_file(url, dest):
6
- if os.path.exists(dest):
7
- print(f"βœ… {dest} already exists.")
8
- return
 
 
 
 
 
 
 
9
 
10
- print(f"Downloading {os.path.basename(dest)}...")
11
 
12
- # Handle Google Drive direct download links
13
- if "drive.google.com" in url:
14
- if "id=" in url:
15
- file_id = url.split("id=")[1].split("&")[0]
16
- else:
17
- file_id = url.split("/")[-2]
18
- url = f"https://drive.google.com/uc?export=download&id={file_id}"
19
-
20
- response = requests.get(url, stream=True)
21
- total_size = int(response.headers.get('content-length', 0))
22
-
23
- os.makedirs(os.path.dirname(dest), exist_ok=True)
24
 
25
- with open(dest, 'wb') as file, tqdm(
26
- desc=dest,
27
- total=total_size,
28
- unit='iB',
29
- unit_scale=True,
30
- unit_divisor=1024,
31
- ) as bar:
32
- for data in response.iter_content(chunk_size=1024):
33
- size = file.write(data)
34
- bar.update(size)
35
-
36
- if __name__ == "__main__":
37
- MODELS = {
38
- "models/sam2.1_hiera_tiny.pt": "https://github.com/ultralytics/assets/releases/download/v8.3.0/sam2.1_hiera_tiny.pt",
39
- "models/player_detector.pt": "https://drive.google.com/uc?export=download&id=1fVBLZtPy9Yu6Tf186oS4siotkioHBLHy",
40
- "models/ball_detector.pt": "https://drive.google.com/uc?export=download&id=1KejdrcEnto2AKjdgdo1U1syr5gODp6EL",
41
- "models/court_keypoint_detector.pt": "https://drive.google.com/uc?export=download&id=1nGoG-pUkSg4bWAUIeQ8aN6n7O1fOkXU0",
42
- "models/yolov8n-pose.pt": "https://github.com/ultralytics/assets/releases/download/v8.2.0/yolov8n-pose.pt"
43
- }
44
-
45
- for path, url in MODELS.items():
46
  try:
47
- download_file(url, path)
 
 
 
 
 
 
 
48
  except Exception as e:
49
- print(f"❌ Failed to download {path}: {e}")
 
 
 
 
 
1
  import os
2
+ from huggingface_hub import hf_hub_download
 
3
 
4
+ def download_models():
5
+ """Download all models from the dedicated Model Repo using huggingface_hub."""
6
+ repo_id = "icanedit2/bakoai-models"
7
+ models_to_download = [
8
+ "player_detector.pt",
9
+ "ball_detector.pt",
10
+ "court_keypoint_detector.pt",
11
+ "swish_ball_rim.pt",
12
+ "swish_pose.pt",
13
+ "yolov8n-pose.pt"
14
+ ]
15
 
16
+ os.makedirs("models", exist_ok=True)
17
 
18
+ token = os.environ.get("HF_TOKEN")
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ for model_file in models_to_download:
21
+ dest_path = os.path.join("models", model_file)
22
+ if os.path.exists(dest_path):
23
+ print(f"βœ… {dest_path} already exists.")
24
+ continue
25
+
26
+ print(f"⏳ Downloading {model_file} from {repo_id}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  try:
28
+ # hf_hub_download automatically handles LFS, Xet, and auth
29
+ path = hf_hub_download(
30
+ repo_id=repo_id,
31
+ filename=model_file,
32
+ local_dir="models",
33
+ token=token
34
+ )
35
+ print(f"βœ… Successfully downloaded {model_file} to {path}")
36
  except Exception as e:
37
+ print(f"❌ Failed to download {model_file}: {e}")
38
+ raise
39
+
40
+ if __name__ == "__main__":
41
+ download_models()
personal_analysis/pipeline.py CHANGED
@@ -30,50 +30,58 @@ from app.api.videos import get_video_info
30
  # ── Model paths ──────────────────────────────────────────────────────────────
31
  settings = get_settings()
32
 
33
- # ---------------------------------------------------------------------------
34
- # Backend root β€” computed from __file__ then immediately healed.
35
- #
36
- # When the FastAPI process is launched from a shell that strips the apostrophe
37
- # in "DON'T TOUCH", Python's __file__ itself arrives mangled (without the ').
38
- # We detect that case and swap in the correct character so that every
39
- # subsequent model path built from _BACKEND_ROOT is already correct.
40
- # ---------------------------------------------------------------------------
41
- def _heal_path(p: str) -> str:
42
- """Return the real on-disk path by fixing the apostrophe-mangling."""
43
- if os.path.exists(p):
44
- return p
45
- fixed = p.replace("OKIDI-DONT TOUCH", "OKIDI-DON'T TOUCH")
46
- if os.path.exists(fixed):
47
- return fixed
48
- return p # give up – let the caller handle it
49
-
50
- _BACKEND_ROOT = _heal_path(
51
- os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
52
- )
53
- print(f"DEBUG: _BACKEND_ROOT = {_BACKEND_ROOT}", file=sys.stderr)
54
-
55
-
56
  def _resolve_model_path(path: str) -> str:
57
  """
58
- Resolve a model path relative to the (already-healed) backend root.
59
-
60
- β€’ Absolute paths are used as-is (after one heal attempt).
61
- β€’ Relative paths are anchored to _BACKEND_ROOT β€” never to CWD.
62
  """
63
  if os.path.isabs(path):
64
- return _heal_path(path)
65
 
66
- # Anchor to the healed backend root
 
67
  candidate = os.path.abspath(os.path.join(_BACKEND_ROOT, path))
 
68
  if os.path.exists(candidate):
69
  return candidate
70
 
71
- # One more heal attempt (shouldn't be needed after root fix, but be safe)
72
- healed = _heal_path(candidate)
73
- if os.path.exists(healed):
74
- return healed
75
-
76
- logger.warning(f"Model file not found at {candidate!r} β€” check model paths")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return candidate
78
 
79
  BALL_RIM_MODEL = _resolve_model_path(settings.swish_ball_rim_model)
@@ -133,6 +141,7 @@ def _write_video(frames: list, out_path: str, fps: float = 30.0):
133
  os.rename(out_path, tmp_path)
134
  result = subprocess.run(
135
  ["ffmpeg", "-y", "-i", tmp_path,
 
136
  "-c:v", "libx264", "-preset", "fast", "-movflags", "+faststart",
137
  "-pix_fmt", "yuv420p", "-an", out_path],
138
  capture_output=True, timeout=300
 
30
  # ── Model paths ──────────────────────────────────────────────────────────────
31
  settings = get_settings()
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def _resolve_model_path(path: str) -> str:
34
  """
35
+ Resolve a model path relative to the backend root.
36
+
37
+ β€’ Absolute paths are used as-is.
38
+ β€’ Relative paths are anchored to the directory containing 'app/'.
39
  """
40
  if os.path.isabs(path):
41
+ return path
42
 
43
+ # Anchor to the backend root (parent of 'personal_analysis/')
44
+ backend_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
45
  candidate = os.path.abspath(os.path.join(_BACKEND_ROOT, path))
46
+
47
  if os.path.exists(candidate):
48
  return candidate
49
 
50
+ # --- DIAGNOSTIC LOGGING ---
51
+ logger.error(f"❌ Model file MISSING: {candidate}")
52
+ logger.info(f"DEBUG: Current Working Directory: {os.getcwd()}")
53
+ logger.info(f"DEBUG: Backend Root: {_BACKEND_ROOT}")
54
+
55
+ models_dir = os.path.join(_BACKEND_ROOT, "models")
56
+ if os.path.exists(models_dir):
57
+ try:
58
+ files = os.listdir(models_dir)
59
+ logger.info(f"DEBUG: Contents of {models_dir}: {files}")
60
+ except Exception as e:
61
+ logger.error(f"DEBUG: Could not list {models_dir}: {e}")
62
+ else:
63
+ logger.error(f"DEBUG: Directory {models_dir} does not even exist!")
64
+
65
+ # Search for the file anywhere in /home/user/app
66
+ logger.info(f"DEBUG: Searching for {os.path.basename(path)} in /home/user/app...")
67
+ found_paths = []
68
+ for root, dirs, files in os.walk("/home/user/app"):
69
+ if os.path.basename(path) in files:
70
+ found_paths.append(os.path.join(root, os.path.basename(path)))
71
+
72
+ if found_paths:
73
+ logger.info(f"βœ… FOUND at alternative locations: {found_paths}")
74
+ return found_paths[0]
75
+ # ---------------------------
76
+
77
+ # Robust fallback: check if we are in a 'DON'T TOUCH' mangled path environment
78
+ # but only if the candidate doesn't exist.
79
+ if "OKIDI-DONT TOUCH" in candidate:
80
+ healed = candidate.replace("OKIDI-DONT TOUCH", "OKIDI-DON'T TOUCH")
81
+ if os.path.exists(healed):
82
+ return healed
83
+
84
+ logger.warning(f"Model file not found at {candidate!r} β€” ensure 'python download_models.py' has been run.")
85
  return candidate
86
 
87
  BALL_RIM_MODEL = _resolve_model_path(settings.swish_ball_rim_model)
 
141
  os.rename(out_path, tmp_path)
142
  result = subprocess.run(
143
  ["ffmpeg", "-y", "-i", tmp_path,
144
+ "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
145
  "-c:v", "libx264", "-preset", "fast", "-movflags", "+faststart",
146
  "-pix_fmt", "yuv420p", "-an", out_path],
147
  capture_output=True, timeout=300
personal_analysis/test_resolve.py CHANGED
@@ -12,17 +12,20 @@ print(f"DEBUG: _BACKEND_ROOT = {_BACKEND_ROOT}")
12
  def _resolve_model_path(path: str) -> str:
13
  if os.path.isabs(path):
14
  return path
15
- full_path = os.path.abspath(os.path.join(_BACKEND_ROOT, path))
16
- print(f"DEBUG: checking {full_path} -> {os.path.exists(full_path)}")
17
- if os.path.exists(full_path):
18
- return full_path
19
 
20
- hardcoded_root = "/home/student/Music/OKIDI-DON'T TOUCH/BAKO-AI-V0.0/back-end"
21
- safe_path = os.path.abspath(os.path.join(hardcoded_root, path))
22
- print(f"DEBUG: checking fallback {safe_path} -> {os.path.exists(safe_path)}")
23
- if os.path.exists(safe_path):
24
- return safe_path
25
- return full_path
 
 
 
 
 
 
 
26
 
27
  rim = _resolve_model_path(settings.swish_ball_rim_model)
28
  print(f"FINAL RESOLVED: {rim}")
 
12
  def _resolve_model_path(path: str) -> str:
13
  if os.path.isabs(path):
14
  return path
 
 
 
 
15
 
16
+ # Anchor to the backend root (parent of personal_analysis/)
17
+ candidate = os.path.abspath(os.path.join(_BACKEND_ROOT, path))
18
+
19
+ if os.path.exists(candidate):
20
+ return candidate
21
+
22
+ # Robust fallback for 'DON'T TOUCH' mangled environments
23
+ if "OKIDI-DONT TOUCH" in candidate:
24
+ healed = candidate.replace("OKIDI-DONT TOUCH", "OKIDI-DON'T TOUCH")
25
+ if os.path.exists(healed):
26
+ return healed
27
+
28
+ return candidate
29
 
30
  rim = _resolve_model_path(settings.swish_ball_rim_model)
31
  print(f"FINAL RESOLVED: {rim}")