8282)
8383TRACK_PROGRESS_MAX_BATCH = int (os .getenv ("TRACK_PROGRESS_MAX_BATCH" , "32" ))
8484
85+ # Label Studio's authenticated upload endpoints are not always reliable as
86+ # ffmpeg streaming inputs: local dev / nginx modes can return partial bodies,
87+ # and prewarm bursts can trigger 429s. Default to one authenticated download for
88+ # LS-hosted uploads, while still allowing explicit streaming for deployments
89+ # where range requests are known to work well.
90+ STREAM_LS_UPLOADS = os .getenv ("LABEL_STUDIO_STREAM_LS_UPLOADS" , "" ).lower () in {"1" , "true" , "yes" , "on" }
91+
8592# Stop-tracking thresholds (see _run_tracking).
8693# SAM2's mask decoder emits a per-frame object_score_logits; convention is
8794# `> 0` = object present, `<= 0` = occluded/absent. We debounce across a few
@@ -727,18 +734,18 @@ def _predict_single_frame(self, task_id, context, video, frame,
727734 def _ls_host_token (self ) -> Tuple [Optional [str ], Optional [str ]]:
728735 """Return (ls_host, ls_token) for authenticated LS asset fetches.
729736
730- Host resolution is env first, then the cache populated from `/setup`.
731- Token resolution is cache first, then env . Either may be None; callers
732- pass them into `self.get_local_path(ls_host=..., ls_access_token=...)`
733- which lets the SDK skip its `http://localhost:8000` default fallback.
734-
735- LS's own `/setup` payload can carry `http://localhost:<port>` when
736- the LS side doesn't have `HOSTNAME` configured — that's useless to a
737- remote ML backend. The host remains env-var-first so operators can
738- override LS's guess via `.env` / `docker-compose`. The token is
739- cache-first: `/setup` sends the ML-backend access token that LS itself
740- wants us to use, while env tokens are often stale or a frontend refresh
741- JWT that this LS deployment may not accept for API/file downloads .
737+ Host/token resolution is env first, then the cache populated from
738+ `/setup` . Either may be None; callers pass them into
739+ `self.get_local_path(ls_host=..., ls_access_token=...)` which lets the
740+ SDK skip its `http://localhost:8000` default fallback.
741+
742+ LS's own `/setup` payload can carry `http://localhost:<port>` when the
743+ LS side doesn't have `HOSTNAME` configured — that's useless to a remote
744+ ML backend. Keep both host and token env-var-first so an operator can
745+ fix credentials through the process environment / `docker-compose` and
746+ restart the backend; otherwise an old `/setup` access token can shadow a
747+ freshly configured `LABEL_STUDIO_API_KEY` and cause 401s on uploaded
748+ files .
742749 """
743750 env_host = (os .getenv ("LABEL_STUDIO_URL" ) or os .getenv ("LABEL_STUDIO_HOST" ) or "" ).rstrip ("/" )
744751 env_token = (
@@ -749,7 +756,7 @@ def _ls_host_token(self) -> Tuple[Optional[str], Optional[str]]:
749756 cached_host = (LS_CONTEXT .get ("url" ) or "" ).rstrip ("/" )
750757 cached_token = LS_CONTEXT .get ("token" ) or ""
751758 host = env_host or cached_host
752- token = cached_token or env_token
759+ token = env_token or cached_token
753760 return (host or None , token or None )
754761
755762 def _download_valid_video (self , raw_url : str , task_id : str ) -> str :
@@ -784,11 +791,13 @@ def _download_valid_video(self, raw_url: str, task_id: str) -> str:
784791 def _resolve_video_source (self , raw_url : str , task_id : str ):
785792 """Resolve a task video URL to a streamable source + auth headers.
786793
787- * Cloud storage URLs (don't look like LS's own host) → stream directly,
788- no headers.
789- * LS-hosted URLs, whether absolute or relative → attach the LS API
790- token; LS guards the /data/upload/* path and returns 401 without it.
791- * No LS url / key configured → fall back to local download.
794+ * External HTTP(S) URLs (don't look like LS's own host) → stream
795+ directly, no headers.
796+ * LS-hosted upload URLs / paths → authenticated local download by
797+ default; direct ffmpeg streaming can be enabled with
798+ `LABEL_STUDIO_STREAM_LS_UPLOADS=true` when the LS deployment supports
799+ reliable range requests.
800+ * Cloud-storage URIs → authenticated local download through the LS SDK.
792801
793802 Resolution order for the LS hostname:
794803 1. `LABEL_STUDIO_URL` env var (explicit operator override)
@@ -806,12 +815,16 @@ def _auth_headers(target_url: str):
806815 return ls_auth_headers (ls_url , api_key , target_url = target_url )
807816
808817 if raw_url .startswith ("http://" ) or raw_url .startswith ("https://" ):
809- # Absolute URL — attach auth iff its host matches the known LS host
810- # (LS returns 404, not 401, to an unauthenticated /data or /upload
811- # fetch, which is how ffprobe streaming fails). Never attach to any
812- # other host: task data can carry external/presigned cloud URLs and
813- # we must not leak the LS token to them.
818+ # Absolute URL — attach auth iff its host matches the known LS host.
819+ # Never attach to any other host: task data can carry external /
820+ # presigned cloud URLs and we must not leak the LS token to them.
814821 attach = should_attach_ls_auth (raw_url , ls_url , bool (api_key ))
822+ if attach and not STREAM_LS_UPLOADS :
823+ logger .info (
824+ "LS-hosted video: downloading once instead of streaming (%s)" ,
825+ raw_url ,
826+ )
827+ return self ._download_valid_video (raw_url , task_id ), None
815828 return raw_url , _auth_headers (raw_url ) if attach else None
816829
817830 if raw_url .startswith (CLOUD_URI_SCHEMES ):
@@ -822,17 +835,20 @@ def _auth_headers(target_url: str):
822835 # it through `/tasks/<id>/presign/` and downloads the result.
823836 return self ._download_valid_video (raw_url , task_id ), None
824837
825- if ls_url and api_key :
838+ if (
839+ ls_url
840+ and api_key
841+ and STREAM_LS_UPLOADS
842+ and raw_url .startswith (("/data/" , "/upload" , "data/" , "upload" ))
843+ ):
826844 full_url = f"{ ls_url } { raw_url } " if raw_url .startswith ("/" ) else f"{ ls_url } /{ raw_url } "
827845 return full_url , _auth_headers (full_url )
828846
829- logger .warning ("streaming not available (no LABEL_STUDIO_URL), falling back to download" )
830- local_path = self .get_local_path (
831- raw_url ,
832- task_id = task_id ,
833- ls_host = ls_url_opt ,
834- ls_access_token = ls_token_for_sdk (ls_url_opt , api_key_opt ),
835- )
847+ if not ls_url :
848+ logger .warning ("streaming not available (no LABEL_STUDIO_URL), falling back to download" )
849+ else :
850+ logger .info ("video source requires Label Studio resolution; downloading once" )
851+ local_path = self ._download_valid_video (raw_url , task_id )
836852 return local_path , None
837853
838854 def _mask_response (self , mask , w , h , from_name , to_name ):
@@ -937,6 +953,12 @@ def _run_tracking(self, session, video, prompts, start_frame, end_frame,
937953 # as propagate_in_video walks through them — the wait for
938954 # "encode all frames upfront" becomes "encode frame 0".
939955 #
956+ # MPS is the exception: SAM2's async JPEG loader can produce
957+ # float64 CPU tensors and then move them to MPS before the
958+ # subsequent `.float()` cast. MPS doesn't support float64, so
959+ # load frames synchronously there; SAM2's sync path preallocates
960+ # a float32 tensor and avoids the dtype hop.
961+ #
940962 # offload_video_to_cpu=True keeps the loaded frames on CPU so
941963 # only the propagation thread touches the GPU. Without this
942964 # the async loader pushes frames to the device with
@@ -946,7 +968,7 @@ def _run_tracking(self, session, video, prompts, start_frame, end_frame,
946968 # mask "jumping" onto a different object.
947969 inference_state = video_predictor .init_state (
948970 video_path = frame_dir ,
949- async_loading_frames = True ,
971+ async_loading_frames = ( DEVICE != "mps" ) ,
950972 offload_video_to_cpu = True ,
951973 )
952974 video_predictor .reset_state (inference_state )
0 commit comments