| """ |
| Triton Python backend for RAI Image Moderation v1 (ShieldGemma2). |
| |
| This wraps the SageMaker inference.py logic into Triton Python backend format. |
| Input: image (BYTES) - base64-encoded image (optionally with custom_policies in JSON) |
| Output: result (BYTES) - JSON list of policy predictions matching SageMaker output format: |
| [{"policy": "...", "prediction": {"Unsafe": 0.9, "Safe": 0.1}}, ...] |
| """ |
|
|
| import json |
| import base64 |
| import os |
| import numpy as np |
| import triton_python_backend_utils as pb_utils |
|
|
|
|
| |
| DEFAULT_POLICIES = { |
| "Hate/Discrimination": "The image must not contain content that promotes attacks based on personal characteristics or content that claims certain protected groups such as gender, race, religion, sexual orientation, etc. are better than others.", |
| "Misinformation": "The image must not contain content seeking to spread misinformation, disinformation, or conspiracy theories that undermine public trust.", |
| "Jailbreak/Prompt Injection": "The image must not contain content attempting to manipulate AI models to disobey explicit instructions or safety policies.", |
| "Personally Identifiable Information (PII)": "The image must not contain content that aims to extract sensitive personal information such as social security numbers, credit card information, driver licenses, passport numbers, and medical information.", |
| "Politics": "The image must not contain content involving political campaigning or lobbying that may interfere with electoral or democratic processes.", |
| "Impersonation": "The image must not contain content that impersonates specific individuals, fictional characters, or organizations.", |
| "Specialist Advice": "The image must not provide advice that would ordinarily be provided by a qualified professional including legal, medical, financial advice.", |
| "High-risk Decisions": "The image must not contain content involving high stakes decision making that may affect an individual's rights, safety, health or well-being.", |
| } |
|
|
|
|
| class TritonPythonModel: |
| """Triton Python backend model class for ShieldGemma2 image safety classification.""" |
|
|
| def initialize(self, args): |
| """ |
| Called once when the model is loaded. |
| """ |
| import torch |
| from transformers import AutoProcessor, ShieldGemma2ForImageClassification |
|
|
| self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| pb_utils.Logger.log_info(f"Loading ShieldGemma2 model on device: {self.device}") |
|
|
| |
| model_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
|
| self.model = ShieldGemma2ForImageClassification.from_pretrained( |
| model_dir, |
| torch_dtype=torch.bfloat16 |
| ).to(self.device).eval() |
|
|
| self.processor = AutoProcessor.from_pretrained(model_dir) |
| pb_utils.Logger.log_info("ShieldGemma2 model loaded successfully") |
|
|
| def execute(self, requests): |
| """ |
| Called for each inference request batch. |
| """ |
| import torch |
| from PIL import Image |
| from io import BytesIO |
|
|
| responses = [] |
|
|
| for request in requests: |
| |
| image_tensor = pb_utils.get_input_tensor_by_name(request, "image") |
| input_bytes = image_tensor.as_numpy()[0].decode("utf-8") |
|
|
| |
| try: |
| input_data = json.loads(input_bytes) |
| if isinstance(input_data, dict): |
| image_b64 = input_data.get('image', input_bytes) |
| custom_policies = input_data.get('custom_policies', None) |
| else: |
| image_b64 = input_bytes |
| custom_policies = None |
| except (json.JSONDecodeError, TypeError): |
| image_b64 = input_bytes |
| custom_policies = None |
|
|
| |
| img_bytes = base64.b64decode(image_b64) |
| pil_image = Image.open(BytesIO(img_bytes)).convert('RGB') |
|
|
| |
| policies_dict = custom_policies if custom_policies else DEFAULT_POLICIES |
|
|
| |
| custom_policies_dict = {} |
| policy_name_mapping = {} |
| for policy_name, policy_text in policies_dict.items(): |
| key = policy_name.lower() |
| for ch in ['/', ' ', '(', ')', '-']: |
| key = key.replace(ch, '_') |
| key = key.replace('__', '_').strip('_') |
| custom_policies_dict[key] = policy_text |
| policy_name_mapping[key] = policy_name |
|
|
| policies_to_evaluate = list(custom_policies_dict.keys()) |
|
|
| |
| model_inputs = self.processor( |
| images=[pil_image], |
| custom_policies=custom_policies_dict, |
| policies=policies_to_evaluate, |
| return_tensors="pt" |
| ).to(self.device) |
|
|
| with torch.inference_mode(): |
| output = self.model(**model_inputs) |
|
|
| probabilities = output.probabilities |
|
|
| |
| results = [] |
| for idx, policy_key in enumerate(policies_to_evaluate): |
| unsafe_prob = 1.0 - float(probabilities[idx][1]) |
| safe_prob = float(probabilities[idx][1]) |
| results.append({ |
| "policy": policy_name_mapping[policy_key], |
| "prediction": {"Unsafe": unsafe_prob, "Safe": safe_prob} |
| }) |
|
|
| |
| result_json = json.dumps(results).encode('utf-8') |
| out_result = pb_utils.Tensor("result", np.array([result_json], dtype=object)) |
|
|
| responses.append(pb_utils.InferenceResponse(output_tensors=[out_result])) |
|
|
| return responses |
|
|
| def finalize(self): |
| """Called when model is unloaded.""" |
| pb_utils.Logger.log_info("ShieldGemma2 model unloaded") |
|
|