| 1 | # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | # SPDX-License-Identifier: Apache-2.0 |
| 3 | # /// script |
| 4 | # requires-python = ">=3.10" |
| 5 | # dependencies = [ |
| 6 | # "data-designer", |
| 7 | # ] |
| 8 | # /// |
| 9 | """Airport Baggage Screening Image Generation Recipe |
| 10 | |
| 11 | Generate synthetic airport baggage-screening style images with controlled |
| 12 | variation over scanner style, bag density, benign clutter, material mix, object |
| 13 | overlap, and high-level defensive threat-type labels. |
| 14 | |
| 15 | Security note: |
| 16 | This recipe is intended for defensive model development, evaluation, |
| 17 | curriculum data, and human-review tooling. Do not use it to plan, optimize, |
| 18 | or describe ways to bypass real screening systems. The prompts avoid |
| 19 | operational bypass details and use high-level threat types rather than |
| 20 | concealment instructions. |
| 21 | |
| 22 | Prerequisites: |
| 23 | - An image-generation provider key for the selected model. The defaults use |
| 24 | OpenRouter, so set OPENROUTER_API_KEY before running. |
| 25 | |
| 26 | Run: |
| 27 | uv run airport_security_scans.py --num-records 10 |
| 28 | """ |
| 29 | |
| 30 | from __future__ import annotations |
| 31 | |
| 32 | import argparse |
| 33 | from pathlib import Path |
| 34 | |
| 35 | import data_designer.config as dd |
| 36 | from data_designer.interface import DataDesigner, DatasetCreationResults |
| 37 | |
| 38 | DEFAULT_MODEL_PROVIDER = "openrouter" |
| 39 | DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview" |
| 40 | DEFAULT_MODEL_ALIAS = "baggage-screening-image-model" |
| 41 | |
| 42 | |
| 43 | def build_model_configs( |
| 44 | *, |
| 45 | model_provider: str, |
| 46 | model_id: str, |
| 47 | model_alias: str, |
| 48 | image_size: str, |
| 49 | aspect_ratio: str, |
| 50 | max_parallel_requests: int, |
| 51 | ) -> list[dd.ModelConfig]: |
| 52 | """Build a provider-agnostic image-generation model config.""" |
| 53 | return [ |
| 54 | dd.ModelConfig( |
| 55 | alias=model_alias, |
| 56 | model=model_id, |
| 57 | provider=model_provider, |
| 58 | inference_parameters=dd.ImageInferenceParams( |
| 59 | extra_body={ |
| 60 | "modalities": ["image", "text"], |
| 61 | "image_config": { |
| 62 | "aspect_ratio": aspect_ratio, |
| 63 | "image_size": image_size, |
| 64 | }, |
| 65 | }, |
| 66 | max_parallel_requests=max_parallel_requests, |
| 67 | ), |
| 68 | skip_health_check=True, |
| 69 | ) |
| 70 | ] |
| 71 | |
| 72 | |
| 73 | def add_category(config_builder: dd.DataDesignerConfigBuilder, name: str, values: list[str]) -> None: |
| 74 | """Add a categorical sampler column.""" |
| 75 | config_builder.add_column( |
| 76 | dd.SamplerColumnConfig( |
| 77 | name=name, |
| 78 | sampler_type=dd.SamplerType.CATEGORY, |
| 79 | params=dd.CategorySamplerParams(values=values), |
| 80 | ) |
| 81 | ) |
| 82 | |
| 83 | |
| 84 | def add_visual_variation_id(config_builder: dd.DataDesignerConfigBuilder) -> None: |
| 85 | """Add a unique row-level key that discourages duplicate image generations.""" |
| 86 | config_builder.add_column( |
| 87 | dd.SamplerColumnConfig( |
| 88 | name="visual_variation_id", |
| 89 | sampler_type=dd.SamplerType.UUID, |
| 90 | params=dd.UUIDSamplerParams(prefix="scan-", short_form=True), |
| 91 | ) |
| 92 | ) |
| 93 | |
| 94 | |
| 95 | def build_config( |
| 96 | *, |
| 97 | model_provider: str = DEFAULT_MODEL_PROVIDER, |
| 98 | model_id: str = DEFAULT_MODEL_ID, |
| 99 | model_alias: str = DEFAULT_MODEL_ALIAS, |
| 100 | image_size: str = "1K", |
| 101 | aspect_ratio: str = "4:3", |
| 102 | max_parallel_requests: int = 10, |
| 103 | ) -> dd.DataDesignerConfigBuilder: |
| 104 | """Build an airport baggage-screening image-generation pipeline.""" |
| 105 | model_configs = build_model_configs( |
| 106 | model_provider=model_provider, |
| 107 | model_id=model_id, |
| 108 | model_alias=model_alias, |
| 109 | image_size=image_size, |
| 110 | aspect_ratio=aspect_ratio, |
| 111 | max_parallel_requests=max_parallel_requests, |
| 112 | ) |
| 113 | config_builder = dd.DataDesignerConfigBuilder(model_configs=model_configs) |
| 114 | add_visual_variation_id(config_builder) |
| 115 | |
| 116 | add_category( |
| 117 | config_builder, |
| 118 | "scanner_style", |
| 119 | [ |
| 120 | "dual-energy X-ray baggage scan with pseudo-color material mapping", |
| 121 | "computed tomography baggage scan slice rendered as pseudo-color X-ray", |
| 122 | "top-down carry-on baggage screening view", |
| 123 | "side-view checked-bag screening image", |
| 124 | ], |
| 125 | ) |
| 126 | |
| 127 | add_category( |
| 128 | config_builder, |
| 129 | "bag_type", |
| 130 | [ |
| 131 | "small carry-on roller bag", |
| 132 | "soft backpack", |
| 133 | "messenger bag", |
| 134 | "hard-shell suitcase", |
| 135 | "duffel bag", |
| 136 | "camera equipment case", |
| 137 | ], |
| 138 | ) |
| 139 | |
| 140 | add_category( |
| 141 | config_builder, |
| 142 | "bag_density", |
| 143 | [ |
| 144 | "sparse packing with many empty regions", |
| 145 | "moderate packing density", |
| 146 | "dense packing with overlapping objects", |
| 147 | "very dense packing with cluttered object boundaries", |
| 148 | ], |
| 149 | ) |
| 150 | |
| 151 | add_category( |
| 152 | config_builder, |
| 153 | "benign_contents", |
| 154 | [ |
| 155 | "clothing, shoes, toiletries, and paperback books", |
| 156 | "laptop, chargers, headphones, notebooks, and snacks", |
| 157 | "camera body, lenses, batteries, cables, and clothing", |
| 158 | "children's toys, folded clothing, tablet, and water bottle", |
| 159 | "sports gear, towel, shoes, and plastic accessories", |
| 160 | "business travel items, documents, laptop, and power adapters", |
| 161 | ], |
| 162 | ) |
| 163 | |
| 164 | add_category( |
| 165 | config_builder, |
| 166 | "material_mix", |
| 167 | [ |
| 168 | "mostly fabric and plastic with a few small metal objects", |
| 169 | "electronics-heavy bag with cables and batteries", |
| 170 | "mixed organic, plastic, and metal materials", |
| 171 | "mostly low-density organic material with scattered dense regions", |
| 172 | "many small overlapping metal and plastic objects", |
| 173 | ], |
| 174 | ) |
| 175 | |
| 176 | add_category( |
| 177 | config_builder, |
| 178 | "threat_type", |
| 179 | [ |
| 180 | "none - clear benign bag with no threat-like visual pattern", |
| 181 | "dense electronics cluster requiring secondary review", |
| 182 | "oversized liquid-container-like region requiring secondary review", |
| 183 | "sharp-object-like silhouette requiring secondary review", |
| 184 | "unknown dense object requiring secondary review", |
| 185 | "clutter and overlapping objects preventing confident clearance", |
| 186 | "organic anomaly requiring secondary review", |
| 187 | "ambiguous tool-like silhouette requiring secondary review", |
| 188 | ], |
| 189 | ) |
| 190 | |
| 191 | add_category( |
| 192 | config_builder, |
| 193 | "image_quality", |
| 194 | [ |
| 195 | "clean scanner output with crisp object boundaries", |
| 196 | "slightly noisy scanner output", |
| 197 | "low-contrast scan with compressed dynamic range", |
| 198 | "scan with mild motion blur", |
| 199 | "scan with color palette shifted toward orange and blue material classes", |
| 200 | ], |
| 201 | ) |
| 202 | |
| 203 | config_builder.add_column( |
| 204 | dd.ImageColumnConfig( |
| 205 | name="baggage_scan", |
| 206 | prompt=AIRPORT_SECURITY_SCAN_PROMPT, |
| 207 | model_alias=model_alias, |
| 208 | ) |
| 209 | ) |
| 210 | |
| 211 | return config_builder |
| 212 | |
| 213 | |
| 214 | def create_dataset( |
| 215 | config_builder: dd.DataDesignerConfigBuilder, |
| 216 | *, |
| 217 | num_records: int, |
| 218 | dataset_name: str, |
| 219 | artifact_path: Path | str | None = None, |
| 220 | ) -> DatasetCreationResults: |
| 221 | data_designer = DataDesigner(artifact_path=artifact_path) |
| 222 | data_designer.validate(config_builder) |
| 223 | return data_designer.create(config_builder, num_records=num_records, dataset_name=dataset_name) |
| 224 | |
| 225 | |
| 226 | AIRPORT_SECURITY_SCAN_PROMPT = """\ |
| 227 | Create a synthetic airport baggage-screening training image that shows only the scan content. |
| 228 | |
| 229 | Image requirements: |
| 230 | - Visual variation ID, for internal diversity only: {{ visual_variation_id }} |
| 231 | - Scanner style: {{ scanner_style }} |
| 232 | - Bag type: {{ bag_type }} |
| 233 | - Bag density: {{ bag_density }} |
| 234 | - Benign contents: {{ benign_contents }} |
| 235 | - Material mix: {{ material_mix }} |
| 236 | - Threat type metadata target, not text to render: {{ threat_type }} |
| 237 | - Image quality: {{ image_quality }} |
| 238 | |
| 239 | Render the image as a realistic pseudo-color baggage scan, not a normal photo. |
| 240 | Show overlapping objects, material-color variation, partial occlusion, and |
| 241 | scanner-like attenuation. The image should be useful for defensive model |
| 242 | development and human-review training. |
| 243 | |
| 244 | Generate exactly one final scan image for this row. Do not return alternate |
| 245 | versions, a grid, a pair of examples, a before/after image, multiple scans, or |
| 246 | multiple panels. Use the visual variation ID only as an internal diversity key |
| 247 | for object placement, scanner angle, and material pattern; never render it as |
| 248 | text. |
| 249 | |
| 250 | The output must be the scan image only. Do not add labels, legends, captions, |
| 251 | classification text, bounding boxes, arrows, callouts, segmentation overlays, |
| 252 | heatmaps, UI panels, scanner controls, watermarks, timestamps, filenames, row |
| 253 | IDs, colored outlines, or any additional layer of text. Do not include |
| 254 | operational airport details, real airport names, passenger names, barcodes, |
| 255 | boarding passes, bypass instructions, or anything that describes how to hide or |
| 256 | evade detection. Use the threat type only to shape the broad visual contents of |
| 257 | the bag scan. |
| 258 | """ |
| 259 | |
| 260 | |
| 261 | def parse_args() -> argparse.Namespace: |
| 262 | parser = argparse.ArgumentParser(description="Generate synthetic airport baggage-screening images.") |
| 263 | parser.add_argument("--num-records", type=int, default=10, help="Number of baggage scan images to generate.") |
| 264 | parser.add_argument("--dataset-name", default="synthetic-baggage-screening-scans", help="Output dataset name.") |
| 265 | parser.add_argument("--artifact-path", type=Path, default=None, help="Optional Data Designer artifact directory.") |
| 266 | parser.add_argument("--model-provider", default=DEFAULT_MODEL_PROVIDER, help="Image model provider name.") |
| 267 | parser.add_argument("--model-id", default=DEFAULT_MODEL_ID, help="Provider model ID.") |
| 268 | parser.add_argument("--model-alias", default=DEFAULT_MODEL_ALIAS, help="Alias used by image columns.") |
| 269 | parser.add_argument("--image-size", default="1K", help="OpenRouter image size tier, such as 1K, 2K, or 4K.") |
| 270 | parser.add_argument("--aspect-ratio", default="4:3", help="Provider-specific aspect ratio value.") |
| 271 | parser.add_argument("--max-parallel-requests", type=int, default=10, help="Maximum parallel image requests.") |
| 272 | return parser.parse_args() |
| 273 | |
| 274 | |
| 275 | def main() -> None: |
| 276 | args = parse_args() |
| 277 | config_builder = build_config( |
| 278 | model_provider=args.model_provider, |
| 279 | model_id=args.model_id, |
| 280 | model_alias=args.model_alias, |
| 281 | image_size=args.image_size, |
| 282 | aspect_ratio=args.aspect_ratio, |
| 283 | max_parallel_requests=args.max_parallel_requests, |
| 284 | ) |
| 285 | results = create_dataset( |
| 286 | config_builder, |
| 287 | num_records=args.num_records, |
| 288 | dataset_name=args.dataset_name, |
| 289 | artifact_path=args.artifact_path, |
| 290 | ) |
| 291 | dataset = results.load_dataset() |
| 292 | print(f"Generated {len(dataset)} synthetic baggage-screening rows.") |
| 293 | print(f"Dataset artifacts: {results.artifact_storage.base_dataset_path}") |
| 294 | |
| 295 | |
| 296 | if __name__ == "__main__": |
| 297 | main() |