CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A Hugging Face Space that serves SAM2 (Segment Anything 2) image and video segmentation. It runs on ZeroGPU via the Gradio SDK, and also exposes its functions as an MCP server and a FastAPI/Swagger API (/docs). It originated as a fork of SkalskiP/florence-sam.
SAM2 is provided by HuggingFace transformers (Sam2Model/Sam2Processor for images, Sam2VideoModel/Sam2VideoProcessor for video), not the standalone sam2/samv2 package.
The Space config lives in the YAML frontmatter of README.md (sdk: gradio, app_file: app.py).
Running
pip install -r requirements.txt
python app.py # launches Gradio UI + MCP server + FastAPI docs at /docs
Model weights are downloaded from the HuggingFace Hub on first use and cached by transformers β there is no manual checkpoint download step. The four variants (tiny, small, base_plus, large) map to SAM2.1 Hub repo IDs (facebook/sam2.1-hiera-*) in samv2_handler.variant_hf_mapping.
ffmpeg_extractor.py is also a standalone Typer CLI:
python ffmpeg_extractor.py extract-frames <video_path> --fps 5
python ffmpeg_extractor.py extract-keyframes <video_path> --threshold 0.3
python ffmpeg_extractor.py extract-keyframes-greedy <video_path>
There are currently no tests in the repo despite pytest being in requirements.txt.
Architecture
The flow is layered: app.py (Gradio/MCP/API surface) β samv2_handler.py (SAM2 orchestration) β ffmpeg_extractor.py / toolbox/ (media + encoding utilities).
app.pyβ defines the two public endpointsprocess_imageandprocess_videoas Gradio Interfaces (api_name=). It handles input coercion (JSON strings β lists, base64 string cleanup) and validation, then delegates to the handler. Models are loaded lazily per-request viaload_im_model/load_vid_model.samv2_handler.pyβ the SAM2 wrapper. Theload_sam_*_modelfunctions each return a(model, processor)tuple.run_sam_im_inferenceruns prompted segmentation (boxes and/or points) via theSam2Processor+Sam2Modelandpost_process_masks.run_sam_video_inferenceextracts frames to a temp dir, loads them into aSam2VideoProcessor.init_video_session, seeds the reference frame with input masks viaadd_inputs_to_inference_session(input_masks=...), then iteratespropagate_in_video_iteratorto track objects.unpack_masksconverts per-frame mask logits into the output detection format; a small_propagate_as_tuplesadapter feeds it(frame_idx, object_ids, mask_logits)tuples (post-processed to original frame size, un-binarized).toolbox/mask_encoding.pyβ masks are exchanged as base64-encoded 1-bit PNG strings.b64_mask_encodewrites a temp PNG and returns base64 bytes;b64_mask_decodereverses it. This is the canonical mask interchange format across image/video endpoints.toolbox/vid_utils.pyβVidInfo(cv2 probe, used by video inference for frame width/height) andVidReader(imageio reader); plus general-purpose video read/write/slice/resize helpers.visualizer.pyβmask_to_xyxy(mask β bounding box) is the only actively-used function. Theannotate_*functions reference modules (mcolors,im_draw_bbox,im_color_mask) that are not imported and are effectively dead/debug-only code.
ZeroGPU + SAM2 critical conventions
These are load-bearing and easy to break:
- Every GPU function is decorated with
@spaces.GPU.process_videouses@spaces.GPU(duration=120)β the caller must have β₯2 min of ZeroGPU time available. process_image/process_videoare wrapped with@torch.inference_mode()and@torch.autocast(device_type="cuda", dtype=torch.bfloat16). The autocast context is also entered at module top inapp.py. This is required to avoid the SAM2RuntimeError: No available kernel. Aborting execution.error (see README changelog).multimask_output=Falseinrun_sam_im_inferenceβ SAM2 returns 3 masks per object whenTrue; the code intentionally returns one mask per prompt (one per box, or a single mask for a points-only prompt).
Output detection format ("Miro's format")
Video inference returns a list of per-frame detections with normalized coordinates:
{"frame": int, "track_id": int, "x": x0/w, "y": y0/h, "w": (x1-x0)/w, "h": (y1-y0)/h, "conf": 1, "mask_b64": "..."}
mask_b64 is omitted when drop_masks=True. Empty masks (no object in that frame) are skipped.
Reference frame & bidirectional tracking
ref_frame_idx (default 0) is the frame the input masks correspond to. When it's nonzero, video inference propagates forward and then again with reverse=True from that frame, so objects are tracked in both directions without resetting state.