diff --git a/sam3/agent/agent_core.py b/sam3/agent/agent_core.py index f0016c7c..2eb1239f 100644 --- a/sam3/agent/agent_core.py +++ b/sam3/agent/agent_core.py @@ -9,6 +9,7 @@ from .client_llm import send_generate_request from .client_sam3 import call_sam_service +from .helpers.filename_utils import sanitize_filename from .viz import visualize @@ -319,12 +320,14 @@ def agent_inference( print(f"🔍 Checking mask {i+1}/{num_masks}...") image_w_mask_i, image_w_zoomed_in_mask_i = visualize(current_outputs, i) + # Sanitize the text prompt to create safe filenames + sanitized_prompt = sanitize_filename(LATEST_SAM3_TEXT_PROMPT) image_w_zoomed_in_mask_i_path = os.path.join( - sam_output_dir, rf"{LATEST_SAM3_TEXT_PROMPT}.png".replace("/", "_") - ).replace(".png", f"_zoom_in_mask_{i + 1}.png") + sam_output_dir, f"{sanitized_prompt}_zoom_in_mask_{i + 1}.png" + ) image_w_mask_i_path = os.path.join( - sam_output_dir, rf"{LATEST_SAM3_TEXT_PROMPT}.png".replace("/", "_") - ).replace(".png", f"_selected_mask_{i + 1}.png") + sam_output_dir, f"{sanitized_prompt}_selected_mask_{i + 1}.png" + ) image_w_zoomed_in_mask_i.save(image_w_zoomed_in_mask_i_path) image_w_mask_i.save(image_w_mask_i_path) @@ -391,13 +394,11 @@ def agent_inference( } image_w_check_masks = visualize(updated_outputs) + # Sanitize the text prompt to create a safe filename + sanitized_prompt = sanitize_filename(LATEST_SAM3_TEXT_PROMPT) image_w_check_masks_path = os.path.join( - sam_output_dir, rf"{LATEST_SAM3_TEXT_PROMPT}.png" - ).replace( - ".png", - f"_selected_masks_{'-'.join(map(str, [i+1 for i in masks_to_keep]))}.png".replace( - "/", "_" - ), + sam_output_dir, + f"{sanitized_prompt}_selected_masks_{'-'.join(map(str, [i+1 for i in masks_to_keep]))}.png" ) image_w_check_masks.save(image_w_check_masks_path) # save the updated json outputs and append to message history diff --git a/sam3/agent/client_sam3.py b/sam3/agent/client_sam3.py index d2f64b77..2b66aef1 100755 --- a/sam3/agent/client_sam3.py +++ b/sam3/agent/client_sam3.py @@ -9,6 +9,7 @@ from sam3.model.box_ops import box_xyxy_to_xywh from sam3.train.masks_ops import rle_encode +from .helpers.filename_utils import sanitize_filename from .helpers.mask_overlap_removal import remove_overlapping_masks from .viz import visualize @@ -59,9 +60,8 @@ def call_sam_service( """ print(f"📞 Loading image '{image_path}' and sending with prompt '{text_prompt}'...") - text_prompt_for_save_path = ( - text_prompt.replace("/", "_") if "/" in text_prompt else text_prompt - ) + # Sanitize the text prompt to create a safe filename + text_prompt_for_save_path = sanitize_filename(text_prompt) os.makedirs( os.path.join(output_folder_path, image_path.replace("/", "-")), exist_ok=True diff --git a/sam3/agent/helpers/filename_utils.py b/sam3/agent/helpers/filename_utils.py new file mode 100644 index 00000000..a9d55074 --- /dev/null +++ b/sam3/agent/helpers/filename_utils.py @@ -0,0 +1,38 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved + +import hashlib +import re + + +def sanitize_filename(text_prompt: str, max_length: int = 200) -> str: + """ + Sanitize a text prompt to be used as a filename. + + Replaces invalid filesystem characters, truncates to max_length, and appends + a hash suffix to ensure uniqueness. + + Args: + text_prompt: The text prompt to sanitize + max_length: Maximum length for the filename (default: 200) + Leaves room for extensions and path components + + Returns: + A sanitized filename-safe string + """ + # Generate hash suffix for uniqueness (8 chars + 1 underscore = 9 chars) + prompt_hash = hashlib.md5(text_prompt.encode('utf-8')).hexdigest()[:8] + hash_suffix = f"_{prompt_hash}" + + # Sanitize: replace invalid chars, collapse whitespace/underscores, trim + sanitized = re.sub(r'[<>:"/\\|?*\x00-\x1f\s]+', '_', text_prompt).strip('_.') + + # Use default if empty after sanitization + sanitized = sanitized or "prompt" + + # Truncate to fit max_length with hash suffix + available_length = max_length - len(hash_suffix) + if len(sanitized) > available_length: + sanitized = sanitized[:available_length] + + return f"{sanitized}{hash_suffix}" + diff --git a/sam3/agent/inference.py b/sam3/agent/inference.py index 0aac1165..c6122670 100644 --- a/sam3/agent/inference.py +++ b/sam3/agent/inference.py @@ -4,6 +4,7 @@ import os from sam3.agent.agent_core import agent_inference +from sam3.agent.helpers.filename_utils import sanitize_filename def run_single_image_inference( @@ -27,8 +28,15 @@ def run_single_image_inference( # Generate output file names image_basename = os.path.splitext(os.path.basename(image_path))[0] - prompt_for_filename = text_prompt.replace("/", "_").replace(" ", "_") - + # Sanitize the text prompt to create a safe filename + prompt_for_filename = sanitize_filename(text_prompt, max_length=150) + + # Build base filename, truncating image_basename if needed to keep total length reasonable + # Leave room for separators and suffixes like "_pred.json" + max_base_length = 200 + suffix_length = len(f"_{prompt_for_filename}_agent_{llm_name}") + if len(image_basename) + suffix_length > max_base_length: + image_basename = image_basename[:max(0, max_base_length - suffix_length)] base_filename = f"{image_basename}_{prompt_for_filename}_agent_{llm_name}" output_json_path = os.path.join(output_dir, f"{base_filename}_pred.json") output_image_path = os.path.join(output_dir, f"{base_filename}_pred.png")