John Ho Claude Opus 4.8 (1M context) commited on
Commit
d37f0a7
·
1 Parent(s): e4ca3dc

Rewrite MCP tool docstrings to survive Gradio's schema parser

Browse files

The four gr.Interface functions are exposed as MCP tools (mcp_server=True),
whose schema is built from each function's docstring + type hints. Gradio's
parser silently mangles anything off-shape, so the tools' schemas were broken:

- video_inference: had no Args section (undocumented params) and a vague
output description; added full output schema, named bbox format, and hints.
- video_annotation: had no docstring at all; added one.
- image_visual_inference: multi-line JSON arg descriptions produced phantom
parameters; collapsed to single-line args and moved schema into prose.
- image_text_inference: Returns section detail was discarded by the parser;
folded field names/output schema into the description prose.

Per the gradio-mcp-tool-docstrings skill: full output schema (with the
albumentations bbox-format URL) lives in the description prose, one line per
Args entry, no Returns section, and real return type hints added. Verified
against a live /gradio_api/mcp/schema dump: correct tool names, complete
descriptions with URLs intact, all args documented, no phantom params.
No runtime behavior or signatures changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +37 -27
app.py CHANGED
@@ -164,10 +164,16 @@ def video_inference(
164
  prompt: str,
165
  timeout_duration: int = 60,
166
  annotation_mode: bool = False,
167
- ):
168
- """
169
- Segments objects in a video using a text prompt.
170
- Returns a list of detection dicts (one per object per frame) and output video path/status.
 
 
 
 
 
 
171
  """
172
  assert type(VID_MODEL) != type(None) and type(VID_PROCESSOR) != type(
173
  None
@@ -262,7 +268,16 @@ def video_inference(
262
  )
263
 
264
 
265
- def video_annotation(input_video, prompt: str, timeout_duration: int = 60):
 
 
 
 
 
 
 
 
 
266
  return video_inference(
267
  input_video, prompt, timeout_duration=timeout_duration, annotation_mode=True
268
  )
@@ -275,22 +290,17 @@ def image_visual_inference(
275
  bboxes=None,
276
  points=None,
277
  point_labels=None,
278
- ):
279
- """
280
- SAM3 image segmentation with visual prompts (points and/or boxes).
281
 
282
- Drop-in match for SAM2's `process_image`:
283
 
284
  Args:
285
- im: Pillow Image
286
- variant: accepted for SAM2 API parity; ignored (SAM3 has a single model)
287
- bboxes: bounding boxes to segment, as a list of dicts (or JSON string):
288
- [{"x0":..., "y0":..., "x1":..., "y1":...}, ...] -- one mask per box
289
- points: points to segment, as a list of dicts (or JSON string):
290
- [{"x":..., "y":...}, ...] -- all points describe a single object/mask
291
- point_labels: list of ints (or JSON string), 1=foreground, 0=background
292
- Returns:
293
- list: a list of base64-encoded mask strings (one per box, then one for points)
294
  """
295
  assert TRK_MODEL is not None and TRK_PROCESSOR is not None, (
296
  "Image tracker model failed to load on startup."
@@ -345,17 +355,17 @@ def image_visual_inference(
345
 
346
 
347
  @spaces.GPU
348
- def image_text_inference(im: Image.Image, prompt: str, conf_threshold: float = 0.5):
349
- """
350
- SAM3 image segmentation with a text prompt (concept segmentation).
 
 
 
351
 
352
  Args:
353
- im: Pillow Image
354
- prompt: concept to segment, e.g. "player in white"
355
- conf_threshold: score threshold for kept instances
356
- Returns:
357
- list: detection dicts [{"track_id":..., "x":..., "y":..., "w":..., "h":...,
358
- "conf":..., "mask_b64":...}, ...] with normalized x/y/w/h
359
  """
360
  assert IMG_MODEL is not None and IMG_PROCESSOR is not None, (
361
  "Image text model failed to load on startup."
 
164
  prompt: str,
165
  timeout_duration: int = 60,
166
  annotation_mode: bool = False,
167
+ ) -> list[dict] | str:
168
+ """Track and segment objects across a video with SAM3 using a natural-language text prompt, returning per-object-per-frame detections (or an annotated video).
169
+
170
+ The prompt is a concept to find (e.g. "player in white", "red car"); every matching instance is tracked across all frames with a stable track_id. This tool has two output shapes selected by annotation_mode. When annotation_mode is false (default) it returns a JSON list of detection objects, one per tracked object per frame. Each detection has: "frame" (integer, 0-based frame index); "track_id" (integer, stable across frames for one object); "x", "y", "w", "h" (floats); "conf" (float, always 1 for video); and "mask_b64" (string). The bounding box ("x","y","w","h") is NORMALIZED 0-1: (x, y) is the top-left corner and (w, h) is the box size, each divided by the video width/height -- i.e. the albumentations "coco" layout [x_min, y_min, width, height] but normalized to 0-1 rather than absolute pixels, and NOT the "pascal_voc" [x_min, y_min, x_max, y_max] layout. Bounding-box formats are documented at https://albumentations.ai/docs/3-basic-usage/bounding-boxes-augmentations/#bounding-box-formats . "mask_b64" is a base64-encoded 1-bit PNG of the binary segmentation mask at the frame resolution; decode it with PIL, e.g. numpy.array(Image.open(io.BytesIO(base64.b64decode(mask_b64)))) to get a 0/255 mask. When annotation_mode is true it instead returns a filesystem path to an mp4 video with the colored mask overlays burned in.
171
+
172
+ Args:
173
+ input_video: The input video to segment (a file path, or an uploaded-file object with a "name" key).
174
+ prompt: Natural-language description of the object(s) to track/segment, e.g. "player in white".
175
+ timeout_duration: Max GPU lease in seconds for this request (60, 120, 180, or 240); longer videos need more.
176
+ annotation_mode: If false (default) return the JSON detections list; if true return a path to an annotated mp4 with mask overlays.
177
  """
178
  assert type(VID_MODEL) != type(None) and type(VID_PROCESSOR) != type(
179
  None
 
268
  )
269
 
270
 
271
+ def video_annotation(input_video, prompt: str, timeout_duration: int = 60) -> str:
272
+ """Track and segment objects across a video with SAM3 using a natural-language text prompt, and return an annotated video with the segmentation masks overlaid.
273
+
274
+ The prompt is a concept to find (e.g. "player in white", "red car"); every matching instance is tracked across all frames and rendered as a colored mask overlay (color keyed by object). This is the annotated-video counterpart of video_inference: it returns a filesystem path to an mp4 with the mask overlays burned in, rather than JSON detections. Use video_inference (annotation_mode false) instead if you need the structured per-frame bounding boxes and base64 masks.
275
+
276
+ Args:
277
+ input_video: The input video to segment (a file path, or an uploaded-file object with a "name" key).
278
+ prompt: Natural-language description of the object(s) to track/segment, e.g. "player in white".
279
+ timeout_duration: Max GPU lease in seconds for this request (60, 120, 180, or 240); longer videos need more.
280
+ """
281
  return video_inference(
282
  input_video, prompt, timeout_duration=timeout_duration, annotation_mode=True
283
  )
 
290
  bboxes=None,
291
  points=None,
292
  point_labels=None,
293
+ ) -> list[str]:
294
+ """Segment objects in a single image with SAM3 using visual prompts (bounding boxes and/or points); a drop-in match for SAM2's process_image.
 
295
 
296
+ Provide at least one of bboxes or points. Each bounding box produces its own mask; all points together describe one object and produce a single mask. The return value is a JSON list of base64-encoded 1-bit-PNG mask strings, ordered as: one mask per bounding box (in the order the boxes were given), then, if points were provided, one final mask for the points prompt. Each string decodes to a binary segmentation mask at the input image resolution; decode it with PIL, e.g. numpy.array(Image.open(io.BytesIO(base64.b64decode(mask_b64)))) to get a 0/255 mask. Box pixel coordinates use (x0, y0) = top-left and (x1, y1) = bottom-right corners of the ORIGINAL input image (albumentations "pascal_voc" [x_min, y_min, x_max, y_max], absolute pixels); point coordinates (x, y) are absolute pixels in the same image. Coordinate formats are documented at https://albumentations.ai/docs/3-basic-usage/bounding-boxes-augmentations/#bounding-box-formats .
297
 
298
  Args:
299
+ im: The RGB image to segment (a PIL Image).
300
+ variant: Accepted for SAM2 API parity and ignored; SAM3 has a single model.
301
+ bboxes: Bounding-box prompts as a list of dicts (or a JSON string), each dict {"x0","y0","x1","y1"} in absolute pixels; one output mask per box.
302
+ points: Point prompts as a list of dicts (or a JSON string), each dict {"x","y"} in absolute pixels; all points together define a single object/mask.
303
+ point_labels: List of ints (or a JSON string) parallel to points, 1=foreground and 0=background; required whenever points is given.
 
 
 
 
304
  """
305
  assert TRK_MODEL is not None and TRK_PROCESSOR is not None, (
306
  "Image tracker model failed to load on startup."
 
355
 
356
 
357
  @spaces.GPU
358
+ def image_text_inference(
359
+ im: Image.Image, prompt: str, conf_threshold: float = 0.5
360
+ ) -> list[dict]:
361
+ """Segment every instance of a concept in a single image with SAM3 using a natural-language text prompt (concept segmentation).
362
+
363
+ The prompt is a concept to find (e.g. "player in white", "red car"); each matching instance becomes one detection. Returns a JSON list of detection objects, each with: "track_id" (integer index of the instance within this image); "x", "y", "w", "h" (floats); "conf" (float 0-1 detection score); and "mask_b64" (string). The bounding box ("x","y","w","h") is NORMALIZED 0-1: (x, y) is the top-left corner and (w, h) is the box size, each divided by the image width/height -- i.e. the albumentations "coco" layout [x_min, y_min, width, height] but normalized to 0-1 rather than absolute pixels, and NOT the "pascal_voc" [x_min, y_min, x_max, y_max] layout. Bounding-box formats are documented at https://albumentations.ai/docs/3-basic-usage/bounding-boxes-augmentations/#bounding-box-formats . "mask_b64" is a base64-encoded 1-bit PNG of the binary segmentation mask at the input image resolution; decode it with PIL, e.g. numpy.array(Image.open(io.BytesIO(base64.b64decode(mask_b64)))) to get a 0/255 mask.
364
 
365
  Args:
366
+ im: The RGB image to segment (a PIL Image).
367
+ prompt: Natural-language concept to segment, e.g. "player in white"; all matching instances are returned.
368
+ conf_threshold: Minimum detection score from 0.0 to 1.0; instances scoring below this are discarded.
 
 
 
369
  """
370
  assert IMG_MODEL is not None and IMG_PROCESSOR is not None, (
371
  "Image text model failed to load on startup."