DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…

|| true)\n ST_FILES=$(echo \"$FILES\" | grep -E '\\.(safetensors|bin)

DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…

|| true)\n echo \"ONNX files: ${ONNX_FILES:-none}\"\n echo \"SafeTensors/bin: ${ST_FILES:-none}\"\n echo \"All files: $FILES\"\n\n # If ONNX list is empty in root, also check /onnx subdirectory\n if [ -z \"$ONNX_FILES\" ]; then\n ONNX_SUB=\"$(bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-list-files.sh \"$HF_ORG\" \"$MODEL_NAME\" onnx | grep -E '\\.onnx

DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…

|| true)\"\n echo \"ONNX in /onnx subdir: ${ONNX_SUB:-none}\"\n fi\n ```\n- Classify the repo into one of these categories:\n\n **Category A: ONNX files available** -> proceed to Step 2a (select ONNX variant)\n **Category B: SafeTensors/PyTorch only (no ONNX)** -> proceed to Step 2b (export to ONNX)\n **Category C: No usable model files** -> inform user, suggest alternative repos\n **Category D: NGC model (not on HuggingFace)** -> proceed to Step 2d (NGC download)\n\n- Download `config.json` — required for architecture detection and label extraction.\n Uses the vetted helper script (validated inputs, HTTPS+TLS, honors `$HF_TOKEN`):\n ```bash\n # HF: download from API via vetted helper. NGC: extracted from archive in Step 2d.\n if [ \"$MODEL_SOURCE\" = \"hf\" ]; then\n bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-download-config.sh \\\n \"$HF_ORG\" \"$MODEL_NAME\" \"models/$MODEL_NAME/config/config.json\"\n else\n echo \"NGC model — config.json will be extracted from the downloaded archive in Step 2d\"\n fi\n # Note: models/$MODEL_NAME/config/ already exists from the MANDATORY mkdir at the top of Step 2\n ```\n- Inspect `config.json` to identify:\n - Model type (e.g., `grounding-dino`, `detr`, `yolos`, `resnet`, `swin`)\n - Architecture class (e.g., `GroundingDinoForObjectDetection`)\n - Number of inputs (single input vs multi-modal)\n\n- **Reject non-detection architectures (fail fast)**: Check the `architectures` field in `config.json` before continuing. If the architecture class ends in a non-detection suffix such as `ForImageClassification`, `ForSemanticSegmentation`, `ForInstanceSegmentation`, `ForPanopticSegmentation`, `ForDepthEstimation`, `ForMaskedLM`, `ForTokenClassification`, or `ForCausalLM`, **abort the pipeline with a clear error and exit non-zero**: `\"deepstream-import-vision-model currently supports object detection models only. Detected architecture: {arch_class}. Classification, segmentation, and other vision tasks are not yet supported.\"` Do not prompt the user. Detection architectures end in `ForObjectDetection` (or, for some DETR-family variants, `ForConditionalDetection` / `ForZeroShotObjectDetection`).\n\n- **Validate the architecture and extract `labels.txt`** — one shared helper does both, so the\n HF and NGC routes cannot drift apart. Run it as soon as `config.json` is in place (for HF that\n is now; for NGC it runs at the end of Step 2d):\n ```bash\n build/.venv_optimum/bin/python \\\n .claude/skills/deepstream-import-vision-model/scripts/model/config-to-labels.py \\\n --config models/$MODEL_NAME/config/config.json \\\n --labels models/$MODEL_NAME/config/labels.txt\n ```\n It exits non-zero on a non-detection architecture (the fail-fast gate above) and on a missing\n label map. **Treat either as fatal** — do not prompt the user, and never fall back to hardcoded\n COCO, ImageNet, or any other default list.\n\n### Step 2a: Select ONNX Variant (Category A)\n- Identify available quantization variants (fp32, fp16, int8, int4, quantized, etc.)\n- **Default preference: fp16**. Apply this logic:\n 1. If fp16 variant exists -> **select it silently**, log: `\"Selected: fp16 (default). All available: [list]\"`\n 2. If fp16 does NOT exist -> **auto-select deterministically** in this priority order: fp32 > int8 > int4 > quantized > first ONNX alphabetically. Log: `\"Selected: {variant} (fp16 unavailable). All available: [list]\"`. Do not prompt the user.\n 3. If only one ONNX file exists -> log it and proceed without asking\n- **Construct the resolved download URL** for the selected variant from the tree listing:\n ```bash\n # The tree API returns entries with a \"path\" field (relative to repo root)\n # Construct the download URL as:\n PATH_FROM_TREE=\"\u003cpath field from tree listing, e.g. onnx/model_fp16.onnx>\"\n ONNX_URL=\"https://huggingface.co/$HF_ORG/$MODEL_NAME/resolve/main/$PATH_FROM_TREE\"\n # Example: path=\"onnx/model_fp16.onnx\" -> URL ends in /resolve/main/onnx/model_fp16.onnx\n # Store this URL for use in Step 3\n ```\n- After URL construction, proceed to **Step 3** (download ONNX)\n\n### Step 2b: Export SafeTensors to ONNX (Category B)\n\nWhen the repo only has `.safetensors` (or `.bin`) files and no ONNX export, convert to ONNX using an **isolated virtual environment** to avoid polluting the host system.\n\n#### 2b-i: Virtual Environment (already built by `setup.sh`, in-container)\n- The shared venv at **`build/.venv_optimum`** is created **inside the DeepStream container** by the\n skill's `setup.sh` (one-time bootstrap) — do **not** create it on the host. Every export command runs\n in-container against it; use `PY=build/.venv_optimum/bin/python` and `build/.venv_optimum/bin/optimum-cli`.\n- It's a single shared venv across all models (`torch`/`transformers`/`onnxruntime` are heavy\n and identical model-to-model). If it's missing, run the bootstrap (from SKILL.md Pre-flight):\n ```bash\n # in-container (via docker run … --entrypoint bash … setup.sh); NOT on the host\n .claude/skills/deepstream-import-vision-model/setup.sh # builds build/.venv_optimum + wkhtmltopdf\n ```\n- For a new model that needs **extra packages** (e.g. `timm` for DETR-family backbones or `onnxsim`), `pip install` them **into the existing shared venv** rather than creating a new one:\n ```bash\n source build/.venv_optimum/bin/activate\n pip install timm # or: pip install onnxsim\n ```\n- The venv lives under `build/.venv_optimum` at the repo root, keeping `models/` clean and excluded from git via the root `.gitignore`\n- All subsequent Python/pip commands in Step 2b must run inside this venv\n- Legacy per-model venvs at `build/.venv_$MODEL_NAME` from older runs are still cleaned up by `.claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh \"$MODEL_NAME\"` for backward compatibility\n\n#### 2b-ii: Download Required Files\n- Download from the HF repo into `models/$MODEL_NAME/hf_model/` using `-P` to avoid changing the working directory:\n ```bash\n mkdir -p models/$MODEL_NAME/hf_model\n HF_BASE=\"https://huggingface.co/$HF_ORG/$MODEL_NAME/resolve/main\"\n # Download model files\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/model.safetensors\"\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/config.json\"\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/preprocessor_config.json\"\n # For text+vision models, also download tokenizer files (failures are non-fatal):\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/tokenizer.json\" || true\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/tokenizer_config.json\" || true\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/vocab.txt\" || true\n wget -P models/$MODEL_NAME/hf_model \"$HF_BASE/special_tokens_map.json\" || true\n ```\n- For sharded models (multiple `.safetensors` files), also download `model.safetensors.index.json` and all shards\n\n#### 2b-iii: SafeTensors -> ONNX Export -- Max 3 Retries\n\n> **optimum was removed from this skill.** It pinned `transformers` below 4.54.0, which blocked the\n> releases fixing its RCE advisories (GHSA-29pf-2h5f-8g72, fixed in 5.3.0; GHSA-fgcw-684q-jj6r, fixed\n> in 5.5.0), and optimum 2.1.0 dropped the `onnx` subcommand entirely. Export now goes through\n> `torch.onnx.export` directly.\n\n- Run the export wrapper (it resolves the shared venv and calls `safetensors_to_onnx.py`):\n ```bash\n bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \\\n models/$MODEL_NAME/hf_model \\\n models/$MODEL_NAME/onnx_export/\n ```\n- It writes `models/$MODEL_NAME/onnx_export/model.onnx` and:\n - reads the export resolution from `preprocessor_config.json` (falling back to 640x640);\n - rejects non-detection architectures before doing any work;\n - **tries the dynamo backend, then falls back to TorchScript** if dynamo specialized the batch\n dimension — it prints `[export] backend=...` so you can see which one produced the graph;\n - consolidates any sidecar `model.onnx.data` back into the `.onnx`;\n - **verifies** the graph has the `pixel_values` input plus `logits` / `pred_boxes` outputs, and\n that the batch dimension really is dynamic. It exits non-zero rather than emitting a\n static-batch graph, because DeepStream needs `batch_size == num_streams` up to `MAX_BS`.\n- Expected output for the default model — note the automatic fallback:\n ```\n [export] backend=dynamo\n [export] pixel_values shape=[2, 3, 640, 640]\n [export] dynamo produced a static batch dimension; trying the next backend\n [export] backend=legacy-torchscript\n [export] pixel_values shape=['batch', 3, 640, 640]\n ```\n- Useful flags (passed straight through): `--opset N` · `--image-size N` · `--max-batch N` ·\n `--static-batch` · `--legacy-torchscript`\n- If export succeeds, copy the ONNX file to the `model/` subdirectory:\n ```bash\n cp models/$MODEL_NAME/onnx_export/model.onnx models/$MODEL_NAME/model/$MODEL_NAME.onnx\n ```\n- **Retry policy**: the two-backend fallback is automatic, so a failure here means both backends\n failed. Retry up to **3 times total** with adjustments between attempts:\n - **Retry 1**: Set `--image-size` explicitly if the error mentions a shape or resize mismatch\n - **Retry 2**: Try a different `--opset` (18 is the default; 17 and below fail on `Resize`)\n - **Retry 3**: Try `--static-batch` if both backends baked in the batch dimension. The engine\n must then be built at a fixed batch size — see `references/engine-build.md`.\n - After 3 failed attempts, fall back to **Step 2b-iv** (hand-written export for an unusual architecture)\n\n#### 2b-iv: Fallback -- Hand-written torch.onnx.export (unusual architectures) -- Max 3 Retries\n- If the wrapper fails after 3 retries because the model does not follow the standard\n `logits` / `pred_boxes` detection contract, write the export inline and adjust the wrapper outputs:\n ```bash\n build/.venv_optimum/bin/python -c \"\n import torch\n from transformers import AutoModelForObjectDetection\n\n model = AutoModelForObjectDetection.from_pretrained('models/$MODEL_NAME/hf_model').eval()\n\n class Wrap(torch.nn.Module):\n def __init__(s, m): super().__init__(); s.m = m\n def forward(s, x):\n o = s.m(pixel_values=x)\n return o.logits, o.pred_boxes # \u003c- adjust for the architecture\n\n # Dummy input matching preprocessor_config.json dimensions\n dummy = torch.randn(1, 3, 800, 800)\n\n # TorchScript backend — the more reliable one for a dynamic batch dimension on\n # DETR-family detectors. Swap to the dynamo block below if this backend fails.\n torch.onnx.export(Wrap(model), dummy, 'models/$MODEL_NAME/model/$MODEL_NAME.onnx',\n opset_version=18, do_constant_folding=True, dynamo=False,\n input_names=['pixel_values'],\n output_names=['logits', 'pred_boxes'],\n dynamic_axes={'pixel_values': {0: 'batch'},\n 'logits': {0: 'batch'},\n 'pred_boxes': {0: 'batch'}})\n\n # dynamo backend (use for text-conditioned models the tracer cannot handle):\n # batch = torch.export.Dim('batch', min=1, max=16)\n # torch.onnx.export(Wrap(model), (torch.randn(2, 3, 800, 800),), '\u003cout>.onnx',\n # opset_version=18, dynamo=True,\n # input_names=['pixel_values'], output_names=['logits', 'pred_boxes'],\n # dynamic_shapes={'pixel_values': {0: batch}})\n \"\n ```\n- Adjust the wrapper's returned tensors, input/output names, and shapes for the architecture\n- Always confirm the batch dimension survived:\n ```bash\n build/.venv_optimum/bin/python -c \"\n import onnx; d = onnx.load('models/$MODEL_NAME/model/$MODEL_NAME.onnx').graph.input[0].type.tensor_type.shape.dim\n print([x.dim_param or x.dim_value for x in d])\" # expect ['batch', 3, H, W]\n ```\n- **Retry policy**: If manual export fails, retry up to **3 times total** with adjustments:\n - **Retry 1**: Try a different `AutoModel` class, or return different tensors from the wrapper\n - **Retry 2**: Switch backend (TorchScript \u003c-> dynamo), remembering `dynamic_axes` vs `dynamic_shapes`\n - **Retry 3**: Drop the dynamic batch entirely and build a fixed-batch engine\n - After 3 failed attempts, **stop and generate a failure report**\n\n> **Gotchas for recent PyTorch/transformers** (verified on torch 2.13.0 + transformers 5.14.1\n> inside `nvcr.io/nvidia/deepstream:9.1-triton-multiarch`):\n> - **Neither backend works for every architecture — try `dynamo=True` first, fall back to `dynamo=False`.** `safetensors_to_onnx.py` does this automatically and reports which backend won.\n> - **`dynamo=True` can silently produce a STATIC batch dimension.** The model's own code may specialize `shape[0]`; `torch.export` then reports *\"you marked batch as dynamic but your code specialized it to a constant\"*. **RT-DETR (`PekingU/rtdetr_r50vd`, the default model) does exactly this** — dynamo yields `[2, 3, 640, 640]`, the TorchScript path yields `['batch', 3, 640, 640]`. Always verify with `onnx.load()`; the exporter asserts it.\n> - **`dynamo=False` does NOT crash on transformers 5.5+ for vision detectors.** An earlier revision of this document claimed it did, via `create_bidirectional_mask`. That applies to **text-conditioned** models (Grounding DINO and friends, which run a BERT text encoder) — not to pure-vision detectors. RT-DETR and YOLOS both export cleanly through TorchScript on transformers 5.14.1.\n> - Under `dynamo=True` the parameter is `dynamic_shapes` (with `torch.export.Dim`), **not** `dynamic_axes`; under `dynamo=False` it is `dynamic_axes`. Passing the wrong one is silently ineffective.\n> - **Use opset 18, not 17.** The dynamo exporter implements >= 18 and auto-upgrades, then its downgrade pass fails outright with `No Adapter To Version 17 for Resize`. Opset 18 is fine on TRT 10.16.\n> - **Call `.eval()` on the wrapper module too**, not just the loaded model — a fresh `nn.Module` defaults to training mode, which changes dropout/batchnorm behaviour during export.\n> - **External data files**: `torch.onnx.export` may produce `model.onnx.data` alongside the `.onnx`. Consolidate before TRT conversion: `m = onnx.load(path, load_external_data=True); onnx.save(m, consolidated_path)`. The exporter does this automatically.\n> - No `ForeignNode` failure was observed for RT-DETR on TRT 10.16 with either backend — see `references/engine-build.md` for the historical issue.\n\n#### 2b-v: Handle Multi-Modal Models (e.g., Grounding DINO)\n- Models that take **both image AND text** inputs need special handling for DeepStream (nvinfer only supports image input)\n- Strategy: **freeze the text prompt** into the ONNX graph as a constant\n 1. Run the model once with a fixed text prompt (e.g., \"person . car . truck .\")\n 2. Export ONNX with the text embeddings baked in as constants\n 3. The resulting ONNX model only needs `pixel_values` as input\n- If freezing is not possible, check `onnx-community/` for pre-converted single-input versions\n- **Inform the user** about the frozen text prompt and its implications (fixed detection classes)\n\n#### 2b-vi: onnxsim — Run After Export When Needed\n\nIf the model has dynamic shape paths that cause TRT `ForeignNode` fusion issues, simplify the ONNX graph with `onnxsim` **before** engine building:\n\n```bash\nsource build/.venv_optimum/bin/activate\npip install onnxsim\npython3 -m onnxsim \\\n models/$MODEL_NAME/model/$MODEL_NAME.onnx \\\n models/$MODEL_NAME/model/${MODEL_NAME}_sim.onnx\n# Use the _sim.onnx for engine building if the original triggers ForeignNode errors\n```\n\nOnly run `onnxsim` if TRT build fails with `ForeignNode` warnings — it is not needed for most models.\n\n#### 2b-vii: Validate ONNX Output\n- After export, validate the ONNX file:\n ```bash\n source build/.venv_optimum/bin/activate\n python3 -c \"\n import onnx\n m = onnx.load('models/$MODEL_NAME/model/$MODEL_NAME.onnx')\n onnx.checker.check_model(m)\n print('Inputs:')\n for i in m.graph.input:\n dims = [d.dim_param or d.dim_value for d in i.type.tensor_type.shape.dim]\n print(f' {i.name}: {dims}')\n print('Outputs:')\n for o in m.graph.output:\n dims = [d.dim_param or d.dim_value for d in o.type.tensor_type.shape.dim]\n print(f' {o.name}: {dims}')\n print('ONNX validation passed!')\n \"\n ```\n- Verify:\n - Single image input (no text/mask inputs -- remove if needed)\n - Output shapes match expected detection format\n - Dynamic batch dimension is present\n\n#### 2b-viii: Cleanup\n- Deactivate the venv after export is complete:\n ```bash\n deactivate\n ```\n- **Keep `build/.venv_optimum` across runs** — it is shared by every SafeTensors → ONNX export and rebuilding it for each model costs minutes and GBs. `cleanup.sh` intentionally does not remove it.\n- `cleanup.sh` removes per-model artifacts (`models/$MODEL_NAME/hf_model`, `models/$MODEL_NAME/onnx_export`, and any legacy `build/.venv_$MODEL_NAME` left over from older runs):\n ```bash\n # Validated script; will refuse unsafe paths. Shared .venv_optimum is preserved.\n bash .claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh \"$MODEL_NAME\"\n # Preview without removing:\n # bash .claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh \"$MODEL_NAME\" --dry-run\n ```\n- The ONNX file is now at `models/$MODEL_NAME/model/$MODEL_NAME.onnx` -- proceed to engine building\n\n### Step 2d: NGC Model Download (Category D)\n\nWhen the model comes from NVIDIA NGC (not HuggingFace), download using the `ngc` CLI if available, or fall back to `wget` for direct file download:\n\n```bash\n# Vetted helper: prefers ngc CLI if installed, else falls back to authenticated\n# HTTPS+TLS via curl against the public NGC catalog API. All inputs validated\n# against ^[A-Za-z0-9._-]+$. See .claude/skills/deepstream-import-vision-model/scripts/model/ngc-download.sh for details.\nbash .claude/skills/deepstream-import-vision-model/scripts/model/ngc-download.sh \\\n \"$NGC_ORG\" \"$NGC_TEAM\" \"$MODEL_NAME\" \"$NGC_VERSION\" \\\n \"models/$MODEL_NAME/ngc_download\"\n\n# Inspect downloaded files\necho \"Downloaded files:\"\nls -lhR models/$MODEL_NAME/ngc_download/\n```\n\n- Identify the ONNX file(s) in the downloaded archive (often inside a subdirectory named after the model version)\n- If the download contains a `.etlt` or `.engine` file only (TAO encrypted format), check if a plain ONNX is also provided; if not, use the TAO-provided engine directly and skip Step 4 (engine build)\n- Copy the ONNX to the model directory:\n ```bash\n NGC_ONNX=$(find models/$MODEL_NAME/ngc_download -name \"*.onnx\" | head -1)\n cp \"$NGC_ONNX\" models/$MODEL_NAME/model/$MODEL_NAME.onnx\n echo \"ONNX: $NGC_ONNX -> models/$MODEL_NAME/model/$MODEL_NAME.onnx\"\n ```\n- Extract `config.json` from the archive and build `labels.txt` (same logic as HF path):\n ```bash\n NGC_CONFIG=$(find models/$MODEL_NAME/ngc_download -name \"config.json\" | head -1)\n if [ -z \"$NGC_CONFIG\" ]; then\n echo \"ERROR: config.json not found in NGC archive — cannot create labels.txt\"\n echo \"Cannot proceed without a label map — aborting. Provide an NGC archive that contains config.json.\"\n exit 1\n else\n cp \"$NGC_CONFIG\" models/$MODEL_NAME/config/config.json\n echo \"config.json extracted from: $NGC_CONFIG\"\n\n # Same helper the HF route uses: gates the architecture, then writes labels.txt.\n # The NGC route reaches config.json later than the HF route, so without this a\n # classification model would build an engine and a parser before anything noticed.\n build/.venv_optimum/bin/python \\\n .claude/skills/deepstream-import-vision-model/scripts/model/config-to-labels.py \\\n --config models/$MODEL_NAME/config/config.json \\\n --labels models/$MODEL_NAME/config/labels.txt || exit 1\n fi\n ```\n\n## Step 3: Download the ONNX Model\n\nThe model directory structure was already created in the MANDATORY block at the top. Do NOT run `mkdir -p` again here — just download the file:\n\n```bash\nwget -O \"models/$MODEL_NAME/model/$MODEL_NAME.onnx\" \"${ONNX_URL}\"\n```\n\nWhere `$ONNX_URL` is the resolved URL constructed at the end of Step 2a (Category A) or derived from the NGC download path (Category D). Categories B and D write the ONNX directly to `models/$MODEL_NAME/model/$MODEL_NAME.onnx` during export/copy — Step 3 only applies to Category A.\n- Also download any external data files if the ONNX model references them (files with `.onnx_data` extension or similar)\n- Verify the download completed successfully and report file size\n\n## Timing\n\nRecord wall-clock time at the start and end of this skill:\n```bash\nSTEP_START=$(date +%s.%N)\n# ... all steps ...\nSTEP_END=$(date +%s.%N)\nSTEP_DURATION=$(python3 -c \"print(round($STEP_END - $STEP_START, 2))\") # bc is not in the container; python3 always is\n```\n\n## Output Summary\n\nWhen complete, print:\n```\n=== HF Model Acquire Complete === [Steps 1-3: ${STEP_DURATION}s]\nModel: $MODEL_NAME\nONNX: models/$MODEL_NAME/model/$MODEL_NAME.onnx ({size} MB)\nInput: {input_name} {input_shape}\nOutput: {output_names} {output_shapes}\nLabels: {num_classes} classes -> models/$MODEL_NAME/config/labels.txt\nReady for: Steps 4-5 — read references/engine-build.md models/$MODEL_NAME/model/$MODEL_NAME.onnx\n```\n(`{size}`, `{input_name}`, `{input_shape}`, `{output_names}`, `{output_shapes}`, `{num_classes}` are filled from the ONNX inspection output — all other fields use bash variables.)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":25302,"content_sha256":"17dab1dcb00953998d8f74136322b9ba71f4904f336a1f974921a1975a5aa4ca"},{"filename":"references/pipeline-run.md","content":"\n# DS Run Pipeline -- Steps 6-7\n\nIntegrate a TensorRT model into DeepStream with parser, validation, and multi-stream benchmarks.\n\nThe model directory is: `$ARGUMENTS`\n\n## Pre-flight: Extract Variables\n\n```bash\n[ -z \"$ARGUMENTS\" ] && { echo \"ERROR: No model directory provided. Usage: /deepstream-import-vision-model models/\u003cmodel_name>/\"; exit 1; }\nMODEL_DIR=\"${ARGUMENTS%/}\"\nMODEL_NAME=$(basename \"$MODEL_DIR\")\n\n# Find ONNX file (exclude _dynamic variants created during export)\nONNX_FILE=$(ls models/$MODEL_NAME/model/*.onnx 2>/dev/null | grep -v '_dynamic' | head -1)\n[ -z \"$ONNX_FILE\" ] && { echo \"ERROR: No ONNX file found in models/$MODEL_NAME/model/ — run Steps 1-3 first (references/model-acquire.md)\"; exit 1; }\nMODEL_FILENAME=$(basename \"$ONNX_FILE\" .onnx)\n\n# Engine + MAX_BS from the shared resolver (see scripts/model/resolve-engine.sh).\neval \"$(bash .claude/skills/deepstream-import-vision-model/scripts/model/resolve-engine.sh \"$MODEL_NAME\")\" || exit 1\n\n# Read PEAK_GPU_STREAMS from trtexec Step 5b log — fixed filename, no timestamp, no wildcard\nTRTEXEC_LOG=\"models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log\"\n[ -f \"$TRTEXEC_LOG\" ] || { echo \"ERROR: trtexec log not found at $TRTEXEC_LOG — run Steps 4-5 first (references/engine-build.md)\"; exit 1; }\nQPS_BS_MAX=$(grep -oP 'Throughput:\\s*\\K[0-9.]+' \"$TRTEXEC_LOG\" | tail -1)\nread IMGS_PER_SEC PEAK_GPU_STREAMS \u003c \u003c(python3 -c \"\nimport math\nimgs = float('$QPS_BS_MAX') * $MAX_BS\nprint(round(imgs, 2), int(math.floor(imgs / 30)))\n\")\n\n# Read spatial dimensions from ONNX inspection\nINSPECT_OUT=$(python3 .claude/skills/deepstream-import-vision-model/scripts/model/inspect-onnx.py \"$ONNX_FILE\")\nINPUT_NAME=$(echo \"$INSPECT_OUT\" | grep -oP 'input_name:\\s*\\K\\S+')\nH=$(echo \"$INSPECT_OUT\" | grep -oP 'height:\\s*\\K[0-9]+')\nW=$(echo \"$INSPECT_OUT\" | grep -oP 'width:\\s*\\K[0-9]+')\n[ -z \"$INPUT_NAME\" ] && { echo \"ERROR: could not parse INPUT_NAME from inspect output\"; exit 1; }\n[ -z \"$H\" ] && { echo \"ERROR: could not parse H — dynamic spatial dims? Set H manually\"; exit 1; }\n[ -z \"$W\" ] && { echo \"ERROR: could not parse W — dynamic spatial dims? Set W manually\"; exit 1; }\n\n# Detect installed CUDA version for parser compilation\nCUDA_VER=$(ls /usr/local/ 2>/dev/null | grep -oP '^cuda-\\K[0-9]+\\.[0-9]+

DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…

| sort -V | tail -1)\n[ -z \"$CUDA_VER\" ] && CUDA_VER=13.2\necho \"CUDA_VER=$CUDA_VER\"\n\n# Count labels\n[ -f \"models/$MODEL_NAME/config/labels.txt\" ] || { echo \"ERROR: labels.txt not found — run Steps 1-3 first (references/model-acquire.md)\"; exit 1; }\nNUM_LABELS=$(wc -l \u003c models/$MODEL_NAME/config/labels.txt)\n\n# Parser function suffix: PascalCase of MODEL_NAME, sanitized for C++ identifiers\n# e.g. yolov8n→Yolov8n rtdetr-l→RtdetrL grounding-dino-base→GroundingDinoBase\nPARSER_FUNC_SUFFIX=$(python3 -c \"\nimport re\nparts = re.sub(r'[^a-zA-Z0-9]', ' ', '$MODEL_NAME').split()\nprint(''.join(p.capitalize() for p in parts))\n\")\n# Sanitize MODEL_NAME for use in C++ source/library filenames — mirrors PARSER_FUNC_SUFFIX logic.\n# e.g. rtdetr-l → rtdetr_l grounding-dino-base → grounding_dino_base\n# printf, NOT echo: echo appends a newline that `tr -c` turns into a trailing '_', yielding\n# e.g. `rtdetr_r50vd_` and a custom-lib-path that never matches the Makefile's TARGET_LIB.\nMODEL_NAME_SAFE=$(printf '%s' \"$MODEL_NAME\" | tr -c 'A-Za-z0-9' '_')\n\n# Video source — default is sample_720p.mp4 (MANDATORY). Never autonomously substitute\n# sample_1080p_h264.mp4 or any other file. DS_VIDEO may only be set when the user explicitly\n# provides a custom video path; it is not a licence to pick a different resolution.\nVIDEO=\"${DS_VIDEO:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}\"\n[ -f \"$VIDEO\" ] || {\n echo \"ERROR: Video file not found: $VIDEO\"\n echo \" Fix 1: Set DS_VIDEO=/path/to/sample_720p.mp4 before running\"\n echo \" Fix 2: Install DeepStream samples (DeepStream 9.1 samples): apt-get install deepstream-9.1-samples\"\n exit 1\n}\n\necho \"Model: $MODEL_NAME\"\necho \"ONNX: $ONNX_FILE (input=$INPUT_NAME, ${H}x${W})\"\necho \"Engine: $ENGINE (MAX_BS=$MAX_BS)\"\necho \"PEAK_GPU_STREAMS: $PEAK_GPU_STREAMS (floor($IMGS_PER_SEC img/s / 30))\"\necho \"Labels: $NUM_LABELS classes\"\n```\n\n> All subsequent commands use these variables — never hardcoded paths or template placeholders.\n\n## Step 6: DeepStream Integration\n\n```bash\nSTEP6_START=$(date +%s.%N)\n```\n\n### 6a: Inspect Model Output Format\n\nVerify output tensor shapes and value ranges before writing the parser:\n```bash\npython3 -c \"\nimport onnxruntime as ort, numpy as np\nsess = ort.InferenceSession('$ONNX_FILE')\ninp = sess.get_inputs()[0]\nout = sess.get_outputs()\nprint(f'Input: {inp.name} shape={inp.shape}')\nfor o in out: print(f'Output: {o.name} shape={o.shape}')\ndummy = np.random.randn(*[d if isinstance(d,int) else 1 for d in inp.shape]).astype(np.float32)\nresult = sess.run(None, {inp.name: dummy})\nfor i,r in enumerate(result): print(f'Output[{i}] range: [{r.min():.4f}, {r.max():.4f}]')\n\"\n```\n\n**CRITICAL**: Determine the correct `net-scale-factor` from the output ranges and model family:\n\n| Model expects | net-scale-factor | Notes |\n|---------------|-----------------|-------|\n| 0–255 input (OpenCV Zoo) | `1.0` | No normalization |\n| 0–1 normalized | `0.00392156862745098` (1/255) | Standard |\n| ImageNet normalized | `0.01752` + offsets | Rare in DS |\n\nWrong scale factor = zero detections. Always verify with KITTI dump (Step 6g) before benchmarks.\n\n### 6b: Write Custom Bounding Box Parser\n\nCreate `models/$MODEL_NAME/parser/nvdsinfer_custombboxparser_${MODEL_NAME_SAFE}.cpp`:\n```cpp\nextern \"C\"\nbool NvDsInferParseCustom${PARSER_FUNC_SUFFIX}(\n std::vector\u003cNvDsInferLayerInfo> const &outputLayersInfo,\n NvDsInferNetworkInfo const &networkInfo,\n NvDsInferParseDetectionParams const &detectionParams,\n std::vector\u003cNvDsInferObjectDetectionInfo> &objectList);\n\nCHECK_CUSTOM_PARSE_FUNC_PROTOTYPE(NvDsInferParseCustom${PARSER_FUNC_SUFFIX});\n```\n\nParser implementation rules:\n- Include `nvdsinfer_custom_impl.h` and use `NvDsInferObjectDetectionInfo` (classId, left, top, width, height, detectionConfidence)\n- Decode model-specific output format into pixel-space bounding boxes:\n - YOLOX-style `[N, num_anchors, 5+C]`: decode grid offsets, exp(w/h), objectness×class_score\n - SSD-style `[N, num_dets, 6]`: extract class, confidence, normalized → pixel coords\n - YOLO with BatchedNMS: parse keepCount, bboxes, scores, classes from 4 output layers\n- **Clip all coordinates** to `[0, networkInfo.width-1]` and `[0, networkInfo.height-1]`\n- Use `detectionParams.perClassPreclusterThreshold` for confidence filtering\n- **NMS**: Dense heads → `cluster-mode=2` (DeepStream NMS). Fused TRT NMS → `cluster-mode=4`\n- **Sanity check for undecoded output**: if bbox values land in [0, 3], the parser is reading grid-space offsets. Most models need `(raw + grid_offset) * stride` for cx/cy and `exp(raw) * stride` for w/h. Verify raw output ranges with Python/ONNX Runtime before writing the parser.\n- Reference: `/opt/nvidia/deepstream/deepstream/sources/libs/nvdsinfer_customparser/nvdsinfer_custombboxparser.cpp`; Header: `sources/includes/nvdsinfer_custom_impl.h`\n\n#### Model-family parser patterns\n\n- **DETR / Conditional DETR**: outputs `logits [B, num_queries, num_classes+1]` and `pred_boxes [B, num_queries, 4]`. Boxes are `(cx, cy, w, h)` normalized to `[0,1]` — convert to `(left, top, width, height)` in pixels. Use **softmax** (not sigmoid) on logits. **Background class is the LAST index** (e.g., index 91 for a 92-class DETR, despite `config.json` showing `\"0\": \"N/A\"`). Skip the background class when iterating. DETR uses Hungarian matching — NMS is not needed; set `cluster-mode=4` (not `nms-iou-threshold=0.0`, which is a legacy key).\n- **OWL-ViT / CLIP-based zero-shot detectors**: outputs `logits [B, num_patches, num_classes]` and `pred_boxes [B, num_patches, 4]`. **Sigmoid** activation (per-class independent scoring, not softmax). Boxes are `(cx, cy, w, h)` normalized `[0,1]`. Use `cluster-mode=2` (NMS with IoU threshold). CLIP preprocessing: `net-scale-factor=0.01459`, `offsets=122.77;116.75;104.09`. Confidence threshold 0.10 works well for general detection; lower to 0.05 for recall-focused tasks.\n- **HF RT-DETR preprocessing quirk**: `RTDetrImageProcessor` may have `do_normalize=false` even though `image_mean`/`image_std` fields exist. When `do_normalize=false`, the model expects `[0,1]` scaled input — set `net-scale-factor=1/255` with no offsets. The ONNX export does NOT bake normalization into the first Conv layer. Verify with ONNX Runtime on a real frame before debugging nvinfer.\n\n#### NGC TAO models — use the built-in parser library\n\nNVIDIA NGC TAO models (trafficcamnet, peoplenet, TrafficCamNet Transformer Lite, etc.) ship with TAO-specific parsers pre-compiled into a system library:\n- **Library path**: `/opt/nvidia/deepstream/deepstream/lib/libnvds_infercustomparser.so` — NOT `libnvds_infercustomparser_tao.so` (even if the NGC YAML config suggests it).\n- Custom parse function names: `NvDsInferParseCustomDDETRTAO`, `NvDsInferParseCustomRTDETRTAO`, etc.\n- **No custom parser compilation needed** — point `custom-lib-path` at the system library and `parse-bbox-func-name` at the TAO function.\n- KITTI dump from `deepstream-app` may emit zero-valued bbox coordinates for DETR/RT-DETR parsers even when detections are correct. Verify visually with JPEG frame extraction instead.\n\n### `network-type` vs `model-type` — use `network-type=0`\n\n- `model-type` is a legacy/unknown key — nvinfer ignores it with a warning.\n- `network-type=0` (Detector) is required to invoke `parse-bbox-func-name`.\n- `network-type=100` (Other) does NOT invoke the custom bbox parser — it requires `output-tensor-meta=1` for external post-processing.\n- **Symptom of the wrong key**: custom parse function is never called (zero detections, no parser debug output) — check that `network-type=0` is set.\n\n### 6c: Create Makefile\n\nWrite `models/$MODEL_NAME/parser/Makefile` using Python to guarantee literal TAB characters in recipe lines (heredoc in bash can produce spaces, which break make):\n```bash\npython3 - \u003c\u003c EOF\nmodel = '$MODEL_NAME'\nmodel_safe = '$MODEL_NAME_SAFE'\ncontent = (\n \"DEEPSTREAM_DIR ?= /opt/nvidia/deepstream/deepstream\\n\"\n \"CUDA_VER ?= 12.8\\n\"\n \"CC := g++\\n\"\n \"CFLAGS := -Wall -std=c++11 -shared -fPIC\\n\"\n \"CFLAGS += -I\\$(DEEPSTREAM_DIR)/sources/includes -I/usr/local/cuda-\\$(CUDA_VER)/include\\n\"\n \"LIBS := -lnvinfer\\n\"\n \"LFLAGS := -Wl,--start-group \\$(LIBS) -Wl,--end-group\\n\"\n f\"SRCFILES := nvdsinfer_custombboxparser_{model_safe}.cpp\\n\"\n f\"TARGET_LIB := libnvdsinfer_{model_safe}_parser.so\\n\"\n \"\\n\"\n \"all: \\$(TARGET_LIB)\\n\"\n \"\\$(TARGET_LIB): \\$(SRCFILES)\\n\"\n \"\\t\\$(CC) -o \\$@ \\$^ \\$(CFLAGS) \\$(LFLAGS)\\n\" # TAB required by make\n \"clean:\\n\"\n \"\\trm -rf \\$(TARGET_LIB)\\n\" # TAB required by make\n)\nwith open(f'models/{model}/parser/Makefile', 'w') as f:\n f.write(content)\nprint(f\"Makefile written: models/{model}/parser/Makefile\")\nEOF\n```\n\n### 6d: Build Parser Library\n\n```bash\nmake -C models/$MODEL_NAME/parser \\\n DEEPSTREAM_DIR=/opt/nvidia/deepstream/deepstream \\\n CUDA_VER=$CUDA_VER\n\n# Verify the symbol is exported\nnm -D models/$MODEL_NAME/parser/libnvdsinfer_${MODEL_NAME_SAFE}_parser.so | grep NvDsInferParseCustom\n```\n\n### 6e: Create nvinfer Config File\n\n```bash\ncat > models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt \u003c\u003c EOF\n[property]\ngpu-id=0\nnet-scale-factor=0.00392156862745098\nmodel-color-format=0\nonnx-file=../model/${MODEL_FILENAME}.onnx\nmodel-engine-file=../benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine\nlabelfile-path=labels.txt\nbatch-size=1\nnetwork-mode=2\nnum-detected-classes=${NUM_LABELS}\nprocess-mode=1\ninterval=0\ngie-unique-id=1\nnetwork-type=0\ncustom-lib-path=../parser/libnvdsinfer_${MODEL_NAME_SAFE}_parser.so\nparse-bbox-func-name=NvDsInferParseCustom${PARSER_FUNC_SUFFIX}\n# 2=DeepStream NMS (dense heads: YOLO, SSD). Use 4 if engine has fused NMS output\ncluster-mode=2\ninfer-dims=3;${H};${W}\nmaintain-aspect-ratio=1\n\n[class-attrs-all]\ntopk=200\nnms-iou-threshold=0.45\npre-cluster-threshold=0.25\nEOF\n```\n\n> **Path note**: All paths are relative to the `config/` directory where this file lives.\n> `net-scale-factor` defaults to `1/255` — update to `1.0` if the model expects 0–255 input (verify via Step 6a).\n\nVerify label count matches:\n```bash\necho \"labels.txt: $NUM_LABELS classes -> num-detected-classes=$NUM_LABELS\"\n```\n\n### 6f: Single-Stream Visual Validation\n\n> **ENCODER RULE:**\n> Primary encoder is `nvv4l2h264enc` (NVENC via V4L2) → `.mp4`. `x264enc` and `openh264enc` are **prohibited**.\n> On systems where `/dev/v4l2-nvenc` is unavailable, the approved fallback is `theoraenc + oggmux`\n> (LGPL; both ship in gst-plugins-base) → `.ogv`. If `theoraenc`/`oggmux` are absent, video creation is skipped.\n> Use `.claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-single-stream.sh` which handles this automatically\n> and emits a `DS_SINGLE_STREAM_MODE=` marker the report parser reads.\n\n**Primary (NVENC available):**\n\n```bash\nmkdir -p models/$MODEL_NAME/samples\n\nGST_DEBUG=1 gst-launch-1.0 \\\n filesrc location=$VIDEO ! \\\n qtdemux ! queue leaky=downstream ! h264parse ! queue ! nvv4l2decoder ! queue ! \\\n m.sink_0 nvstreammux name=m batch-size=1 width=1280 height=720 ! queue ! \\\n nvinfer config-file-path=models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt ! queue ! \\\n nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! \\\n nvdsosd ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! \\\n nvv4l2h264enc ! h264parse ! mp4mux ! \\\n filesink location=models/$MODEL_NAME/samples/${MODEL_NAME}_output.mp4 sync=0\n```\n\n**Fallback (NVENC unavailable — `/dev/v4l2-nvenc` missing, `theoraenc`/`oggmux` present):**\n\nOutput extension switches from `.mp4` to `.ogv` (Ogg/Theora container). `theoraenc` consumes planar `I420`, not `NV12`.\n\n```bash\nGST_DEBUG=1 gst-launch-1.0 \\\n filesrc location=$VIDEO ! \\\n qtdemux ! queue leaky=downstream ! h264parse ! queue ! nvv4l2decoder ! queue ! \\\n m.sink_0 nvstreammux name=m batch-size=1 width=1280 height=720 ! queue ! \\\n nvinfer config-file-path=models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt ! queue ! \\\n nvvideoconvert ! nvdsosd ! nvvideoconvert ! \\\n \"video/x-raw, format=I420\" ! theoraenc quality=48 ! oggmux ! \\\n filesink location=models/$MODEL_NAME/samples/${MODEL_NAME}_output.ogv sync=0\n```\n\nExtract a frame to visually confirm bounding boxes — auto-detect which output file exists:\n\n```bash\nSAMPLE_OUT=$(ls models/$MODEL_NAME/samples/${MODEL_NAME}_output.{mp4,ogv} 2>/dev/null | head -1)\n\ncase \"$SAMPLE_OUT\" in\n *.mp4)\n gst-launch-1.0 \\\n filesrc location=\"$SAMPLE_OUT\" ! \\\n qtdemux ! h264parse ! nvv4l2decoder ! videoconvert ! \"video/x-raw,format=RGB\" ! \\\n jpegenc quality=95 ! \\\n multifilesink location=models/$MODEL_NAME/samples/frame_%04d.jpg max-files=3\n ;;\n *.ogv)\n gst-launch-1.0 \\\n filesrc location=\"$SAMPLE_OUT\" ! \\\n oggdemux ! theoradec ! videoconvert ! \"video/x-raw,format=RGB\" ! \\\n jpegenc quality=95 ! \\\n multifilesink location=models/$MODEL_NAME/samples/frame_%04d.jpg max-files=3\n ;;\nesac\n```\n\nIf **no detections appear**, the most common cause is wrong `net-scale-factor` — update the config and re-run.\n\n### 6g: KITTI Dump — Verify Detections Programmatically\n\nRun a KITTI dump to confirm detections exist before multi-stream benchmarks.\n\n> **Note:** `gie-kitti-output-dir` is a `deepstream-app` `[application]`\n> property — it is **not** read by `nvinfer` directly. Appending it to the\n> nvinfer config and running a `gst-launch-1.0 ... nvinfer ...` pipeline\n> silently produces zero KITTI files. Use the `ds-kitti-dump.sh` helper,\n> which wraps `deepstream-app` with the correct `[application]` section.\n\n```bash\nmkdir -p models/$MODEL_NAME/samples/kitti_output\n\nbash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-kitti-dump.sh \\\n models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt \\\n models/$MODEL_NAME/samples/kitti_output \\\n 100 \\\n \"$VIDEO\"\n\n# Summarise detection results\nKITTI_FILES=$(ls models/$MODEL_NAME/samples/kitti_output/*.txt 2>/dev/null | wc -l)\necho \"KITTI frames written: $KITTI_FILES\"\necho \"Top detected classes:\"\ncat models/$MODEL_NAME/samples/kitti_output/*.txt 2>/dev/null \\\n | awk '{print $1}' | sort | uniq -c | sort -rn | head -10\n```\n\n**Validation gate**: If `KITTI_FILES == 0` or all files are empty, detections are broken. Do NOT proceed to Step 7.\n\n```bash\n# MANDATORY hard stop — do not comment out or remove this check\nif [ \"$KITTI_FILES\" -eq 0 ]; then\n echo \"ERROR: KITTI validation FAILED — zero detection files written.\"\n echo \"Fix net-scale-factor, parser output format, or config before retrying.\"\n echo \"Do NOT proceed to Step 7 benchmarks with broken detections.\"\n exit 1\nfi\nFRAMES_WITH_DETECTIONS=$(grep -rl '.' models/$MODEL_NAME/samples/kitti_output/ 2>/dev/null | wc -l)\nDETECTION_RATE=$(python3 -c \"print(round($FRAMES_WITH_DETECTIONS/$KITTI_FILES*100,1))\")\necho \"Detection rate: $FRAMES_WITH_DETECTIONS / $KITTI_FILES frames = ${DETECTION_RATE}%\"\nif python3 -c \"exit(0 if $FRAMES_WITH_DETECTIONS/$KITTI_FILES >= 0.9 else 1)\"; then\n echo \"KITTI validation PASSED (>= 90% frames with detections)\"\nelse\n echo \"ERROR: Detection rate ${DETECTION_RATE}% \u003c 90% threshold. Fix parser before proceeding.\"\n exit 1\nfi\n```\n\n```bash\nSTEP6_END=$(date +%s.%N)\nSTEP6_DURATION=$(python3 -c \"print(round($STEP6_END - $STEP6_START, 2))\") # bc is not in the container; python3 always is\necho \"[Step 6] completed in ${STEP6_DURATION}s\"\n```\n\n### DeepStream Troubleshooting\n\n| Symptom | Fix |\n|---------|-----|\n| Zero detections | Wrong `net-scale-factor` — check model family table in Step 6a |\n| Engine rebuilds every run | `model-engine-file` path wrong — verify relative path from `config/` |\n| Parser crash | Output tensor shape mismatch — re-check Step 6a output shapes |\n| Wrong bounding box positions | Grid/stride decoding mismatch — verify model architecture docs |\n| `\"layers num: 0\"` | Harmless for dynamic-shape engines — do not debug |\n| deepstream-app segfaults | Use `gst-launch-1.0` instead (transformer models) |\n\n## Step 7: Multi-Stream DeepStream Benchmark\n\n### 7b: Create DS Benchmark Config\n\nCreate one nvinfer config for all DS benchmark runs. `batch-size` is overridden at runtime via the nvinfer GStreamer element property.\n\nThis config is **derived from the Step 6e config** rather than re-authored — every `[property]` and\n`[class-attrs-all]` value is identical. Only the relative paths and `batch-size` differ, because this\ncopy lives one directory deeper (`benchmarks/ds/` instead of `config/`). Deriving it keeps the two\nfrom drifting apart:\n\n```bash\nmkdir -p models/$MODEL_NAME/benchmarks/ds\n\nsed -e 's#^onnx-file=\\.\\./#onnx-file=../../#' \\\n -e 's#^model-engine-file=\\.\\./benchmarks/engines/#model-engine-file=../engines/#' \\\n -e 's#^labelfile-path=labels\\.txt#labelfile-path=../../config/labels.txt#' \\\n -e \"s#^batch-size=1\\$#batch-size=${MAX_BS}#\" \\\n -e 's#^custom-lib-path=\\.\\./#custom-lib-path=../../#' \\\n models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt \\\n > models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt\n```\n\nThe five derived keys, for reference:\n\n| Key | Step 6e (`config/`) | Step 7b (`benchmarks/ds/`) |\n|---|---|---|\n| `onnx-file` | `../model/…` | `../../model/…` |\n| `model-engine-file` | `../benchmarks/engines/…` | `../engines/…` |\n| `labelfile-path` | `labels.txt` | `../../config/labels.txt` |\n| `batch-size` | `1` | `${MAX_BS}` |\n| `custom-lib-path` | `../parser/…` | `../../parser/…` |\n\n> **Path note**: Paths are relative to `benchmarks/ds/` where this config lives.\n\n### Queue Placement Rules (MANDATORY)\n\nEvery pipeline stage must be separated by `queue` elements. Use `leaky=downstream` after `qtdemux` to drop excess frames under GPU saturation; all other queues use no leaky setting (threading only). Always set `batched-push-timeout=-1` on `nvstreammux`. **Never include** `nvmultistreamtiler`, `nvdsosd`, or extra `nvvideoconvert` in benchmark runs — only use for single-stream visual validation (Step 6f).\n\n### 7c: Two-Run DS Benchmark\n\nOnly **2 DS pipeline runs** characterise DS overhead vs trtexec.\n\nBoth runs go through `deepstream-app` with `[application] enable-perf-measurement=1` (wrapped by `.claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh`). FPS is parsed from the canonical `**PERF:` lines DeepStream emits at the configured measurement interval. This replaces the older `gst-launch-1.0 ... ! fpsdisplaysink` path so the runtime no longer depends on `gstreamer1.0-plugins-bad`.\n\n> **PERF line format**: `**PERF: \u003cfps_run> (\u003cfps_avg>)` — one float per active source. The helper script averages the per-stream instantaneous FPS across the last few measurement windows; the parser below mirrors that contract.\n\n**DS Run 1 — Calibration at PEAK_GPU_STREAMS streams:**\n\n> **CRITICAL**: Use `$PEAK_GPU_STREAMS` directly. Do NOT pre-apply any efficiency discount (no ×0.6, ×0.7, etc.). Run 1 *measures* the real overhead — do not guess it.\n\n> Log filenames are **fixed** — no timestamp variation. Always `ds_s${N}_run1.log` and `ds_s${N}_run2.log` in `benchmarks/ds/`. The report-generation skill reads these exact paths.\n\n```bash\n# Hard constraint: num_streams \u003c= engine max batch size — always\nN=$(python3 -c \"print(min($PEAK_GPU_STREAMS, $MAX_BS))\")\nLOG_RUN1=\"models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run1.log\"\n\nSTEP7_RUN1_START=$(date +%s.%N)\nbash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh \\\n models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt \\\n \"$N\" \\\n \"$LOG_RUN1\" \\\n \"$VIDEO\"\n\nFPS_RUN1=$(grep -oP '\\*\\*PERF:\\s*\\K[0-9.]+' \"$LOG_RUN1\" | tail -10 | python3 -c \"\nimport sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)\")\npython3 -c \"exit(0 if float('$FPS_RUN1') > 0 else 1)\" || \\\n { echo \"ERROR: FPS parsing failed for Run 1 — check $LOG_RUN1\"; exit 1; }\n\nTOTAL_FPS_RUN1=$(python3 -c \"print(round(float('$FPS_RUN1') * $N, 2))\")\nRT_STREAMS=$(python3 -c \"import math; print(min(int(math.floor(float('$TOTAL_FPS_RUN1') / 30)), $MAX_BS))\")\necho \"DS Run 1: $N streams | FPS/stream=$FPS_RUN1 | total=$TOTAL_FPS_RUN1 img/s | RT_STREAMS=$RT_STREAMS\"\nSTEP7_RUN1_END=$(date +%s.%N)\nSTEP7_RUN1_DURATION=$(python3 -c \"print(round($STEP7_RUN1_END - $STEP7_RUN1_START, 2))\") # bc is not in the container; python3 always is\necho \"[Step 7 Run 1] completed in ${STEP7_RUN1_DURATION}s\"\n```\n\n**DS Run 2 — Validation at RT_STREAMS:**\n```bash\nN=$RT_STREAMS\nLOG_RUN2=\"models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run2.log\"\n\nSTEP7_RUN2_START=$(date +%s.%N)\nbash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh \\\n models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt \\\n \"$N\" \\\n \"$LOG_RUN2\" \\\n \"$VIDEO\"\n\nFPS_RUN2=$(grep -oP '\\*\\*PERF:\\s*\\K[0-9.]+' \"$LOG_RUN2\" | tail -10 | python3 -c \"\nimport sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)\")\npython3 -c \"exit(0 if float('$FPS_RUN2') > 0 else 1)\" || \\\n { echo \"ERROR: FPS parsing failed for Run 2 — check $LOG_RUN2\"; exit 1; }\n\nTOTAL_FPS_RUN2=$(python3 -c \"print(round(float('$FPS_RUN2') * $N, 2))\")\nRT_CONFIRMED=$(python3 -c \"print('YES' if float('$FPS_RUN2') >= 30 else 'NO')\")\necho \"DS Run 2: $N streams | FPS/stream=$FPS_RUN2 | total=$TOTAL_FPS_RUN2 img/s | Real-time: $RT_CONFIRMED\"\nSTEP7_RUN2_END=$(date +%s.%N)\nSTEP7_RUN2_DURATION=$(python3 -c \"print(round($STEP7_RUN2_END - $STEP7_RUN2_START, 2))\") # bc is not in the container; python3 always is\necho \"[Step 7 Run 2] completed in ${STEP7_RUN2_DURATION}s\"\n```\n\n> **NVDEC saturation on fast nano models**: very fast models (YOLO-nano family, etc.) can saturate NVDEC before GPU. Symptom: DS aggregate FPS plateaus at the same value regardless of stream count (e.g., 6,976 at 128 streams, 7,060 at 200 streams). In this case, `PEAK_GPU_STREAMS` from trtexec is an overestimate — Run 1 at that count will show fps/stream well below 30. The `RT_STREAMS = floor(TOTAL_FPS_RUN1 / 30)` formula above produces the correct NVDEC-limited ceiling. Do not pre-apply an efficiency factor to `PEAK_GPU_STREAMS` to compensate — the 2-run method measures overhead, it does not guess it.\n\n**If Run 2 is still not real-time** (FPS/stream \u003c 30): **converge to the true ceiling — do NOT halve.**\n\n> **Why not halve:** halving overshoots badly. If Run 2 at 38 streams measures 29.6 fps/stream\n> (total 1124.8 img/s — only 1.3% under real-time), the real ceiling is ~37 streams, not 19.\n> Halving to 19 discards ~half the GPU's real capacity and reports a misleadingly low number.\n> Instead, recompute the target from Run 2's *measured* total throughput\n> (`floor(TOTAL_FPS_RUN2 / 30)`), which is strictly below the count that just failed, then\n> step down by 1 until real-time. This lands on the true ceiling in 1–2 short retries.\n\n```bash\n# Bounded convergence: recompute from measured throughput, then decrement by 1.\nRETRIES=0\nwhile [ \"$RT_CONFIRMED\" = \"NO\" ] && [ \"$RETRIES\" -lt 5 ]; do\n # First correction: jump to floor(measured_total/30); afterwards step down by 1.\n NEXT=$(python3 -c \"import math; print(int(math.floor(float('$TOTAL_FPS_RUN2') / 30)))\")\n [ \"$NEXT\" -ge \"$N\" ] && NEXT=$((N - 1)) # guarantee strict progress below the failing count\n RT_STREAMS=$(python3 -c \"print(max(1, $NEXT))\")\n N=$RT_STREAMS\n echo \"Run 2 not real-time (fps/stream=$FPS_RUN2) — converging to $N streams\"\n LOG_RUN2=\"models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run2.log\"\n bash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh \\\n models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt \\\n \"$N\" \\\n \"$LOG_RUN2\" \\\n \"$VIDEO\"\n FPS_RUN2=$(grep -oP '\\*\\*PERF:\\s*\\K[0-9.]+' \"$LOG_RUN2\" | tail -10 | python3 -c \"\nimport sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)\")\n TOTAL_FPS_RUN2=$(python3 -c \"print(round(float('$FPS_RUN2') * $N, 2))\")\n RT_CONFIRMED=$(python3 -c \"print('YES' if float('$FPS_RUN2') >= 30 else 'NO')\")\n RETRIES=$((RETRIES + 1))\n echo \"Retry $RETRIES: $N streams | FPS/stream=$FPS_RUN2 | Real-time: $RT_CONFIRMED\"\n [ \"$N\" -le 1 ] && break\ndone\n```\n\n**CONSTRAINT**: `num_streams \u003c= engine_max_bs` always. Already enforced above via `min(RT_STREAMS, MAX_BS)`.\n\n```bash\nTRTEXEC_QPS=$(grep -oP 'Throughput:\\s*\\K[0-9.]+' \"$TRTEXEC_LOG\" | tail -1)\nTRTEXEC_IMGS=$(python3 -c \"print(round(float('$TRTEXEC_QPS') * $MAX_BS, 2))\")\nDS_EFF_RUN1=$(python3 -c \"print(round(float('$TOTAL_FPS_RUN1') / float('$TRTEXEC_IMGS') * 100, 1))\")\nDS_EFF_RUN2=$(python3 -c \"print(round(float('$TOTAL_FPS_RUN2') / float('$TRTEXEC_IMGS') * 100, 1))\")\n```\n\n## Timing and Output Summary\n\n```bash\nTOTAL_67_DURATION=$(python3 -c \"print(round($STEP6_DURATION + $STEP7_RUN1_DURATION + $STEP7_RUN2_DURATION, 2))\") # bc is not in the container; python3 always is\n```\n\nWhen complete, print:\n```\n=== DeepStream Integration Complete ===\nModel: $MODEL_NAME | Engine: $ENGINE\ntrtexec: $TRTEXEC_IMGS img/s @ BS=$MAX_BS\nDS Run 1 (PEAK): $PEAK_GPU_STREAMS streams | $FPS_RUN1 fps/s | eff $DS_EFF_RUN1%\nDS Run 2 (RT): $RT_STREAMS streams | $FPS_RUN2 fps/s | RT: $RT_CONFIRMED | eff $DS_EFF_RUN2%\nTiming: Step6=${STEP6_DURATION}s Run1=${STEP7_RUN1_DURATION}s Run2=${STEP7_RUN2_DURATION}s Total=${TOTAL_67_DURATION}s\nReady for: Step 8 — read references/report-generation.md models/$MODEL_NAME/\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":27817,"content_sha256":"434d7b844cfb7f1983b4c736680c000f8ea92b86552a59a738d2a2ac2fad0d59"},{"filename":"references/README.md","content":"# deepstream-import-vision-model — Reference Documents\n\nDetailed phase guides for the `deepstream-import-vision-model` skill. Read the relevant file before starting each pipeline phase.\n\n| Document | Pipeline Steps | When to read |\n|---|---|---|\n| [model-acquire.md](model-acquire.md) | Steps 1–3 | Downloading from HuggingFace or NGC; ONNX vs SafeTensors detection and export |\n| [engine-build.md](engine-build.md) | Steps 4–5 | TensorRT dynamic engine build; `trtexec` BS=1 and BS=MAX\\_BS benchmarks |\n| [pipeline-run.md](pipeline-run.md) | Steps 6–7 | Custom `nvinfer` bbox parser; single-stream validation; multi-stream benchmark sweep |\n| [report-generation.md](report-generation.md) | Step 8 | 5 benchmark charts; Markdown → HTML → PDF report generation |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":774,"content_sha256":"0f4e9f8a134f4486ede6039ea93ea095d19aaaabe8253d3082eacbb6716e3538"},{"filename":"references/report-generation.md","content":"\n# NV Import Vision Model Report -- Step 8\n\nGenerate benchmark report with charts, HTML, and PDF from completed benchmarks.\n\nThe model directory is: `$ARGUMENTS`\n\n> ## ⛔ STRICT HTML+PDF RULE — NO EXCEPTIONS, NO DEVIATIONS\n>\n> **HTML and PDF MUST be generated via the canonical pipeline script. Do NOT write your own HTML generator.**\n>\n> **The ONLY permitted way to generate the HTML + PDF:**\n> ```bash\n> python3 .claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py \\\n> models/$MODEL_NAME/reports/benchmark_report.md \\\n> .claude/skills/deepstream-import-vision-model/scripts/report/report-style.css \\\n> models/$MODEL_NAME/reports/ \\\n> $MODEL_NAME\n> ```\n> This produces:\n> - `models/$MODEL_NAME/reports/benchmark_report.html` — styled with report_style.css, charts embedded as base64\n> - `models/$MODEL_NAME/reports/benchmark_report_${MODEL_NAME}.pdf` — via wkhtmltopdf\n>\n> **FORBIDDEN — never do any of these:**\n> - Write your own `generate_html.py` or any custom markdown-to-HTML converter script\n> - Call `wkhtmltopdf` directly — use `md-to-html-pdf.py` which already calls it correctly\n> - Use `md-to-pdf.sh` — GFM+Mermaid design doc tool only, wrong CSS\n> - Use `pandoc`, `pdflatex`, or any other converter\n>\n> The `report_style.css` provides the ONLY correct CSS (dark navy headers #283593, alternating rows #e8eaf6, dark code blocks #263238). Any other CSS produces wrong-looking reports.\n\n## 8a: Report Structure — 12 Mandatory Sections\n\nThe report must contain exactly these 12 sections in order:\n\n1. **Model Configuration** — model name, source (HF repo / NGC), architecture, ONNX source, input/output shapes, classes, custom parser name, cluster mode, precision, engine profile\n2. **System Configuration** — GPU (name + VRAM), Driver, CUDA, TensorRT, DeepStream, OS, Python, PyTorch, ONNX versions\n3. **Preprocessing** — net-scale-factor, offsets, color format, normalization details (with reference to the preprocessing table in deepstream-import-vision-model/SKILL.md)\n4. **Engine Build Summary** — source format, conversion path, engine filename (with max_bs postfix), engine size (MB), FP16 flag, builder_optimization_level if non-default, timing cache path\n5. **trtexec Results** — two runs (BS=1 and BS=MAX_BS) with: QPS, Images/s, GPU Compute mean/P99 (ms). Do NOT include H2D/D2H latency or Host Latency. Show PEAK_GPU_STREAMS derivation:\n ```\n PEAK_GPU_STREAMS = floor(QPS_at_MAX_BS × MAX_BS / 30)\n = floor(imgs_per_sec_at_MAX_BS / 30)\n ```\n6. **PEAK_GPU_STREAMS Derivation** — explicit calculation block showing formula, inputs, and result. If a second engine was built, show both PEAK_GPU_STREAMS computations.\n7. **Single-Stream Validation** — KITTI frame count, frames with detections, top-10 detected classes (from KITTI dump), validation result (PASS/FAIL)\n8. **DeepStream Benchmark Results** — two runs:\n - **DS Run 1 (Calibration at PEAK_GPU_STREAMS)**: streams, batch, FPS/stream, total img/s, real-time (YES/NO)\n - **DS Run 2 (Validation at RT_STREAMS)**: streams, batch, FPS/stream, total img/s, real-time (YES)\n9. **trtexec vs DeepStream Comparison** — 3-column table: trtexec | DS Run 1 | DS Run 2, rows: engine, batch/streams, total imgs/s, FPS/stream, real-time ≥30fps, DS Efficiency %\n10. **Efficiency Analysis** — efficiency formula, Run 1 and Run 2 percentages, breakdown of the gap (NVDEC + mux + GStreamer overhead), GPU-bound vs pipeline-bound verdict\n11. **Pipeline Timing** — per-step wall-clock duration and total:\n | Step | Description | Duration |\n |------|-------------|----------|\n | 1-3 | HF Model Acquire (download + inspect ONNX) | {time}s |\n | 4 | Engine build | {time}s |\n | 5 | trtexec BS=1 + BS=MAX_BS | {time}s |\n | 6 | Parser + config + visual validation + KITTI | {time}s |\n | 7 Run 1 | DS Calibration (PEAK_GPU_STREAMS streams) | {time}s |\n | 7 Run 2 | DS Validation (RT_STREAMS streams) | {time}s |\n | 8 | Report generation | {time}s |\n | **Total** | **End-to-end** | **{total}s** |\n12. **Reference Commands** — exact reproducible commands:\n - trtexec engine build (full command with all flags and paths)\n - trtexec benchmark BS=1 and BS=MAX_BS\n - DeepStream single-stream validation (`gst-launch-1.0` with filesink + OSD)\n - DeepStream multi-stream benchmark (`deepstream-app` with `enable-perf-measurement=1` via `ds-perf-run.sh`, PEAK_GPU_STREAMS and RT_STREAMS variants)\n - nvinfer config key fields (as an ini code block)\n - Custom parser build command (`make` with DEEPSTREAM_DIR and CUDA_VER)\n - Use actual absolute paths from the model directory, never placeholders\n\n## Pre-flight: Extract Variables from Benchmark Logs\n\nBefore generating any output, derive all variables by reading completed benchmark files. These variables are used by every section below.\n\n```bash\nSTEP8_START=$(date +%s.%N)\n\nMODEL_DIR=\"${ARGUMENTS%/}\"\nMODEL_NAME=$(basename \"$MODEL_DIR\")\n\n# Engine + MAX_BS + MODEL_FILENAME from the shared resolver (scripts/model/resolve-engine.sh).\neval \"$(bash .claude/skills/deepstream-import-vision-model/scripts/model/resolve-engine.sh \"$MODEL_NAME\")\" || exit 1\n\n# Extract input name and spatial dims from ONNX (needed for reference commands in the report)\nONNX_FILE=$(ls models/$MODEL_NAME/model/*.onnx 2>/dev/null | grep -v '_dynamic' | head -1)\nif [ -n \"$ONNX_FILE\" ]; then\n INSPECT_OUT=$(python3 .claude/skills/deepstream-import-vision-model/scripts/model/inspect-onnx.py \"$ONNX_FILE\" 2>/dev/null)\n INPUT_NAME=$(echo \"$INSPECT_OUT\" | grep -oP 'input_name:\\s*\\K\\S+')\n H=$(echo \"$INSPECT_OUT\" | grep -oP 'height:\\s*\\K[0-9]+')\n W=$(echo \"$INSPECT_OUT\" | grep -oP 'width:\\s*\\K[0-9]+')\nfi\nINPUT_NAME=${INPUT_NAME:-\"images\"} # fallback\nH=${H:-\"640\"}; W=${W:-\"640\"} # fallback — update if model uses different resolution\n\n# Parse trtexec BS=1 log — fixed filename trtexec_b1.log (no timestamp, no wildcard needed)\nTRTEXEC_LOG_BS1=\"models/$MODEL_NAME/benchmarks/b1/trtexec_b1.log\"\n[ -f \"$TRTEXEC_LOG_BS1\" ] || { echo \"ERROR: $TRTEXEC_LOG_BS1 not found — run Steps 4-5 first (references/engine-build.md)\"; exit 1; }\nQPS_BS1=$(grep -oP 'Throughput:\\s*\\K[0-9.]+' \"$TRTEXEC_LOG_BS1\" | tail -1)\nGPU_MEAN_BS1=$(grep -oP 'GPU Compute Time:.*mean = \\K[0-9.]+' \"$TRTEXEC_LOG_BS1\" | tail -1)\n\n# Parse trtexec BS=MAX_BS log — fixed filename trtexec_b${MAX_BS}.log\nTRTEXEC_LOG_BSMAX=\"models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log\"\n[ -f \"$TRTEXEC_LOG_BSMAX\" ] || { echo \"ERROR: $TRTEXEC_LOG_BSMAX not found — run Steps 4-5 first (references/engine-build.md)\"; exit 1; }\nQPS_BS_MAX=$(grep -oP 'Throughput:\\s*\\K[0-9.]+' \"$TRTEXEC_LOG_BSMAX\" | tail -1)\nGPU_MEAN_BS_MAX=$(grep -oP 'GPU Compute Time:.*mean = \\K[0-9.]+' \"$TRTEXEC_LOG_BSMAX\" | tail -1)\nGPU_P99_BS_MAX=$(grep -oP 'GPU Compute Time:.*percentile\\(99%\\) = \\K[0-9.]+' \"$TRTEXEC_LOG_BSMAX\" | tail -1)\n[ -z \"$QPS_BS_MAX\" ] && { echo \"ERROR: Could not parse Throughput from $TRTEXEC_LOG_BSMAX — log may be empty or malformed\"; exit 1; }\n[ -z \"$MAX_BS\" ] && { echo \"ERROR: Could not parse batch size from engine filename: $ENGINE\"; exit 1; }\n\nread IMGS_PER_SEC PEAK_GPU_STREAMS \u003c \u003c(python3 -c \"\nimport math\nimgs = float('$QPS_BS_MAX') * $MAX_BS\nprint(round(imgs, 2), int(math.floor(imgs / 30)))\n\")\n\n# Parse DeepStream Run 1 and Run 2 FPS from logs written by pipeline-run\n# Fixed filename pattern: benchmarks/ds/ds_s{N}_run1.log and ds_s{N}_run2.log\n# Use glob to find them (N varies per model) then extract N from filename\nDS_LOG_RUN1=$(ls models/$MODEL_NAME/benchmarks/ds/ds_s*_run1.log 2>/dev/null | head -1)\n# Step 7's convergence loop can leave SEVERAL run2 logs (e.g. s17 -> s11 failed -> s9 passed).\n# Lexicographic `head -1` picks ds_s11 over ds_s9, reporting the NON-real-time run as the\n# validated result. Convergence steps downward, so the converged log is the newest — pick by mtime.\n# (`sort -V | tail -1` is NOT correct here: it re-selects the highest N, i.e. the failing run.)\nDS_LOG_RUN2=$(ls -t models/$MODEL_NAME/benchmarks/ds/ds_s*_run2.log 2>/dev/null | head -1)\n[ -z \"$DS_LOG_RUN1\" ] && { echo \"ERROR: No DS Run 1 log found at benchmarks/ds/ds_s*_run1.log — run Steps 6-7 first (references/pipeline-run.md)\"; exit 1; }\n[ -z \"$DS_LOG_RUN2\" ] && { echo \"ERROR: No DS Run 2 log found at benchmarks/ds/ds_s*_run2.log — run Steps 6-7 first (references/pipeline-run.md)\"; exit 1; }\n\nN_RUN1=$(basename \"$DS_LOG_RUN1\" | grep -oP 'ds_s\\K[0-9]+(?=_run1)')\nN_RUN2=$(basename \"$DS_LOG_RUN2\" | grep -oP 'ds_s\\K[0-9]+(?=_run2)')\n[[ \"$N_RUN1\" =~ ^[0-9]+$ ]] || { echo \"ERROR: Could not parse stream count from $(basename \"$DS_LOG_RUN1\") — expected filename pattern ds_s\u003cN>_run1.log\"; exit 1; }\n[[ \"$N_RUN2\" =~ ^[0-9]+$ ]] || { echo \"ERROR: Could not parse stream count from $(basename \"$DS_LOG_RUN2\") — expected filename pattern ds_s\u003cN>_run2.log\"; exit 1; }\nRT_STREAMS=$N_RUN2\n\n# deepstream-app **PERF: format is `**PERF: fps_run0 (fps_avg0) fps_run1 (fps_avg1) ...`\n# Capture stream-0 instantaneous FPS (\\K after `**PERF:`) — 1 value per line — so\n# tail -10 always covers exactly 10 measurement windows regardless of stream count.\n# Multiply by stream count for total throughput.\nFPS_RAW_RUN1=$(grep -oP '\\*\\*PERF:\\s*\\K[0-9.]+' \"$DS_LOG_RUN1\" | tail -10 | python3 -c \"\nimport sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)\")\nFPS_RAW_RUN2=$(grep -oP '\\*\\*PERF:\\s*\\K[0-9.]+' \"$DS_LOG_RUN2\" | tail -10 | python3 -c \"\nimport sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)\")\n\nTOTAL_FPS_RUN1=$(python3 -c \"print(round(float('$FPS_RAW_RUN1') * $N_RUN1, 2))\")\nTOTAL_FPS_RUN2=$(python3 -c \"print(round(float('$FPS_RAW_RUN2') * $N_RUN2, 2))\")\n\necho \"=== Report Variables ===\"\necho \"MODEL_NAME=$MODEL_NAME MAX_BS=$MAX_BS\"\necho \"BS=1: QPS=$QPS_BS1 GPU mean=${GPU_MEAN_BS1}ms\"\necho \"BS=$MAX_BS: QPS=$QPS_BS_MAX imgs/s=$IMGS_PER_SEC PEAK_GPU_STREAMS=$PEAK_GPU_STREAMS\"\necho \"DS Run 1: FPS/stream=$FPS_RAW_RUN1 streams=$N_RUN1 total=$TOTAL_FPS_RUN1 img/s\"\necho \"DS Run 2: FPS/stream=$FPS_RAW_RUN2 streams=$N_RUN2 total=$TOTAL_FPS_RUN2 img/s RT_STREAMS=$RT_STREAMS\"\n```\n\nThen immediately write `benchmark_data.json` before generating charts (so charts can load it if needed):\n\n```bash\nmkdir -p models/$MODEL_NAME/reports\npython3 \u003c\u003c 'EOF'\nimport json, os\n\ndef to_num(v, cast=float):\n \"\"\"Return cast(v) or None if v is empty/invalid — prevents malformed JSON.\"\"\"\n try:\n return cast(v) if v and str(v).strip() else None\n except (ValueError, TypeError):\n return None\n\ndata = {\n \"model_name\": os.environ.get(\"MODEL_NAME\", \"\"),\n \"engine\": os.environ.get(\"ENGINE\", \"\"),\n \"max_bs\": to_num(os.environ.get(\"MAX_BS\"), int),\n \"trtexec\": {\n \"bs1\": {\n \"qps\": to_num(os.environ.get(\"QPS_BS1\")),\n \"gpu_mean_ms\": to_num(os.environ.get(\"GPU_MEAN_BS1\"))\n },\n \"bsmax\": {\n \"qps\": to_num(os.environ.get(\"QPS_BS_MAX\")),\n \"gpu_mean_ms\": to_num(os.environ.get(\"GPU_MEAN_BS_MAX\")),\n \"p99_ms\": to_num(os.environ.get(\"GPU_P99_BS_MAX\")),\n \"imgs_per_sec\": to_num(os.environ.get(\"IMGS_PER_SEC\"))\n }\n },\n \"peak_gpu_streams\": to_num(os.environ.get(\"PEAK_GPU_STREAMS\"), int),\n \"deepstream\": {\n \"run1\": {\n \"streams\": to_num(os.environ.get(\"N_RUN1\"), int),\n \"total_fps\": to_num(os.environ.get(\"TOTAL_FPS_RUN1\")),\n \"fps_per_stream\": to_num(os.environ.get(\"FPS_RAW_RUN1\"))\n },\n \"run2\": {\n \"streams\": to_num(os.environ.get(\"N_RUN2\"), int),\n \"total_fps\": to_num(os.environ.get(\"TOTAL_FPS_RUN2\")),\n \"fps_per_stream\": to_num(os.environ.get(\"FPS_RAW_RUN2\"))\n }\n }\n}\nout_path = os.path.join(\"models\", os.environ.get(\"MODEL_NAME\", \"unknown\"),\n \"reports\", \"benchmark_data.json\")\nwith open(out_path, \"w\") as f:\n json.dump(data, f, indent=2)\nprint(\"benchmark_data.json written\")\nEOF\n```\n> `\u003c\u003c 'EOF'` (quoted) prevents bash expansion — Python reads all variables via `os.environ.get()`, applies `to_num()` for safe numeric conversion (returns `None` instead of producing malformed JSON when a variable is unset), then uses `json.dump` to guarantee valid output.\n\n## 8c-1: Chart Generation (MANDATORY)\n\nAll Python scripts in this step run inside the **shared venv** at `build/.venv_optimum` (which holds `matplotlib`, `numpy`, `markdown`, and `onnxruntime`). Activate it once before running any report scripts:\n\n```bash\nsource build/.venv_optimum/bin/activate\n```\n\nGenerate exactly **5 charts** using `matplotlib` in `models/{model_name}/reports/charts/`. Use the script at `.claude/skills/deepstream-import-vision-model/scripts/report/generate-benchmark-charts.py` or generate manually. Chart names are fixed — do not rename them.\n\n| Filename | Content | Chart type |\n|----------|---------|------------|\n| `chart_trtexec_bs1_vs_bsmax.png` | Bar chart: QPS at BS=1 vs BS=MAX_BS (side by side) | Grouped bar |\n| `chart_trtexec_throughput.png` | GPU-only images/sec at MAX_BS, with PEAK_GPU_STREAMS annotation (dashed line at y=PEAK_GPU_STREAMS×30) | Single bar or line |\n| `chart_ds_streams_vs_fps.png` | Line chart: X=stream count (PEAK_GPU_STREAMS, RT_STREAMS), Y=FPS/stream. Red dashed line at 30fps threshold. | Line + markers |\n| `chart_trt_vs_ds.png` | Grouped bars: trtexec total imgs/s \\| DS Run 1 total imgs/s \\| DS Run 2 total imgs/s | Grouped bar |\n| `chart_efficiency.png` | DS efficiency %: 2 bars (Run 1 efficiency, Run 2 efficiency), dashed line at 100% | Bar |\n\nDo NOT generate H2D/D2H transfer overhead charts.\n\nChart style requirements:\n- Figure size: `figsize=(10, 6)`, DPI: 150\n- Title: two-line format via `two_line_title(model_name, subtitle)` — model name on line 1, chart description on line 2 (prevents long titles from clipping outside figure bounds)\n- Axis labels: 13px; Bar value labels: bold, 12-13px, positioned above bars\n- Grid: `axis='y', alpha=0.3`; `plt.tight_layout()` before save\n- Use `matplotlib.use('Agg')` (no display needed)\n\n## 8c-1b: Markdown Report (MANDATORY)\n\nGenerate `benchmark_report.md` before the HTML. This file must contain all 12 sections filled with actual values — no placeholders allowed.\n\nFirst, gather system info not already captured in pre-flight:\n\n```bash\nGPU_INFO=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader | head -1)\nGPU_NAME=$(echo \"$GPU_INFO\" | cut -d, -f1 | xargs)\nGPU_VRAM=$(echo \"$GPU_INFO\" | cut -d, -f2 | xargs)\nDRIVER_VER=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -1 | xargs)\nCUDA_VER=$(nvcc --version 2>/dev/null | grep -oP 'release \\K[0-9.]+' || echo \"N/A\")\nTRT_VER=$(trtexec 2>&1 | head -3 | grep -oP 'TensorRT v\\K[0-9.]+' || echo \"N/A\")\nDS_VER=$(deepstream-app --version-all 2>/dev/null | grep -oP 'DeepStreamSDK \\K[0-9.]+' || echo \"N/A\")\nENGINE_SIZE_MB=$(du -m \"$ENGINE\" | cut -f1)\nIMGS_PER_SEC_BS1=$(python3 -c \"print(round(float('$QPS_BS1') * 1, 2))\")\nGPU_P99_BS1=$(grep -oP 'GPU Compute Time:.*percentile\\(99%\\) = \\K[0-9.]+' \"$TRTEXEC_LOG_BS1\" | tail -1)\nGPU_P99_BS1=${GPU_P99_BS1:-\"N/A\"} # fallback if log too short to have P99\nEFFICIENCY_RUN1=$(python3 -c \"print(round(float('$TOTAL_FPS_RUN1') / float('$IMGS_PER_SEC') * 100, 1))\")\nEFFICIENCY_RUN2=$(python3 -c \"print(round(float('$TOTAL_FPS_RUN2') / float('$IMGS_PER_SEC') * 100, 1))\")\nRT_LABEL_RUN1=$(python3 -c \"print('YES' if float('$FPS_RAW_RUN1') >= 30 else 'NO')\")\nRT_LABEL_RUN2=$(python3 -c \"print('YES' if float('$FPS_RAW_RUN2') >= 30 else 'NO')\")\n```\n\nThen write the markdown (use unquoted `\u003c\u003c MDEOF` so bash expands variables):\n\n```bash\ncat > models/$MODEL_NAME/reports/benchmark_report.md \u003c\u003c MDEOF\n# ${MODEL_NAME} Benchmark Report\n\nGenerated: $(date '+%Y-%m-%d %H:%M:%S')\n\n---\n\n## 1. Model Configuration\n\n| Parameter | Value |\n|-----------|-------|\n| **Model Name** | ${MODEL_NAME} |\n| **Source** | (fill from Steps 1-3 log) |\n| **Architecture** | (fill from config.json model_type) |\n| **ONNX Source** | models/${MODEL_NAME}/model/ |\n| **Precision** | FP16 |\n| **Engine File** | $(basename $ENGINE) |\n| **Engine Profile** | min=1x3x640x640 opt=${MAX_BS}x3x640x640 max=${MAX_BS}x3x640x640 |\n| **Custom Parser** | libnvdsinfer_${MODEL_NAME}_parser.so |\n| **Cluster Mode** | (fill from nvinfer config) |\n\n## 2. System Configuration\n\n| Parameter | Value |\n|-----------|-------|\n| **GPU** | ${GPU_NAME} |\n| **VRAM** | ${GPU_VRAM} |\n| **Driver** | ${DRIVER_VER} |\n| **CUDA** | ${CUDA_VER} |\n| **TensorRT** | ${TRT_VER} |\n| **DeepStream** | ${DS_VER} |\n\n## 3. Preprocessing\n\n| Parameter | Value |\n|-----------|-------|\n| **net-scale-factor** | (fill from nvinfer config) |\n| **offsets** | (fill from nvinfer config) |\n| **Color Format** | (fill from nvinfer config) |\n| **Input Resolution** | 640×640 |\n\n## 4. Engine Build Summary\n\n| Parameter | Value |\n|-----------|-------|\n| **Source Format** | ONNX |\n| **Engine File** | $(basename $ENGINE) |\n| **Engine Size** | ${ENGINE_SIZE_MB} MB |\n| **FP16** | Enabled |\n| **MAX Batch Size** | ${MAX_BS} |\n| **Workspace** | 32768 MiB |\n| **Timing Cache** | models/${MODEL_NAME}/benchmarks/engines/timing.cache |\n\n## 5. trtexec Results\n\n| Metric | BS=1 | BS=${MAX_BS} |\n|--------|------|------|\n| **QPS (queries/s)** | ${QPS_BS1} | ${QPS_BS_MAX} |\n| **Images/s** | ${IMGS_PER_SEC_BS1} | ${IMGS_PER_SEC} |\n| **GPU Compute Mean (ms)** | ${GPU_MEAN_BS1} | ${GPU_MEAN_BS_MAX} |\n| **GPU Compute P99 (ms)** | ${GPU_P99_BS1} | ${GPU_P99_BS_MAX} |\n\n> Note: H2D/D2H latency excluded — trtexec run with \\`--noDataTransfers\\` to match DeepStream (GPU-to-GPU data flow, no host transfers).\n\n![trtexec BS=1 vs BS=${MAX_BS}](charts/chart_trtexec_bs1_vs_bsmax.png)\n\n## 6. PEAK_GPU_STREAMS Derivation\n\n\\`\\`\\`\nPEAK_GPU_STREAMS = floor(imgs_per_sec_at_MAX_BS / 30)\n = floor(${IMGS_PER_SEC} / 30)\n = ${PEAK_GPU_STREAMS} streams\n\\`\\`\\`\n\n![trtexec throughput at BS=${MAX_BS}](charts/chart_trtexec_throughput.png)\n\n## 7. Single-Stream Validation\n\n| Parameter | Value |\n|-----------|-------|\n| **Video Source** | sample_720p.mp4 (1280×720) |\n| **KITTI Output Dir** | models/${MODEL_NAME}/samples/kitti_output/ |\n| **Total Frames** | (fill from kitti dump) |\n| **Frames with Detections** | (fill from kitti dump) |\n| **Detection Rate** | (fill — must be ≥ 90%) |\n| **Visual Capture Mode** | (fill: `nvv4l2h264enc MP4` OR `theoraenc OGV (NVENC unavailable)` OR `skipped (no encoder available)`) |\n| **Visual Capture Artifact** | (fill: `samples/${MODEL_NAME}_output.mp4` for NVENC path; `samples/${MODEL_NAME}_output.ogv` for theoraenc fallback; `N/A` if skipped) |\n| **Validation Result** | PASS |\n\n> **Encoder reporting rule (MANDATORY):** The Visual Capture Mode field MUST be exactly one of:\n> - `nvv4l2h264enc MP4` — NVENC succeeded; artifact is `.mp4`\n> - `theoraenc OGV (NVENC unavailable)` — if `DS_SINGLE_STREAM_MODE=theoraenc-fallback`; use `.ogv` path from `DS_SINGLE_STREAM_OUTPUT=`\n> - `skipped (no encoder available)` — if `DS_SINGLE_STREAM_MODE=skipped`; no artifact file\n> `x264enc` and `openh264enc` are prohibited and must never appear in this field.\n\n## 8. DeepStream Benchmark Results\n\n### DS Run 1 — Calibration at PEAK_GPU_STREAMS (${N_RUN1} streams)\n\n| Metric | Value |\n|--------|-------|\n| **Streams** | ${N_RUN1} |\n| **Batch Size** | ${N_RUN1} |\n| **FPS / Stream** | ${FPS_RAW_RUN1} |\n| **Total Images/s** | ${TOTAL_FPS_RUN1} |\n| **Real-Time (≥30 fps/stream)** | ${RT_LABEL_RUN1} |\n\n### DS Run 2 — Validation at RT_STREAMS (${N_RUN2} streams)\n\n| Metric | Value |\n|--------|-------|\n| **Streams** | ${N_RUN2} |\n| **Batch Size** | ${N_RUN2} |\n| **FPS / Stream** | ${FPS_RAW_RUN2} |\n| **Total Images/s** | ${TOTAL_FPS_RUN2} |\n| **Real-Time (≥30 fps/stream)** | ${RT_LABEL_RUN2} |\n\n![DeepStream FPS/stream vs stream count](charts/chart_ds_streams_vs_fps.png)\n\n## 9. trtexec vs DeepStream Comparison\n\n| Metric | trtexec BS=${MAX_BS} | DS Run 1 (${N_RUN1} streams) | DS Run 2 (${N_RUN2} streams) |\n|--------|---------------------|------------------------------|------------------------------|\n| **Engine** | $(basename $ENGINE) | $(basename $ENGINE) | $(basename $ENGINE) |\n| **Batch / Streams** | BS=${MAX_BS} | ${N_RUN1} streams | ${N_RUN2} streams |\n| **Total imgs/s** | ${IMGS_PER_SEC} | ${TOTAL_FPS_RUN1} | ${TOTAL_FPS_RUN2} |\n| **FPS / stream** | $(python3 -c \"print(round(float('$IMGS_PER_SEC')/${MAX_BS},1))\") | ${FPS_RAW_RUN1} | ${FPS_RAW_RUN2} |\n| **Real-Time ≥30fps** | YES | ${RT_LABEL_RUN1} | ${RT_LABEL_RUN2} |\n| **DS Efficiency %** | — | ${EFFICIENCY_RUN1}% | ${EFFICIENCY_RUN2}% |\n\n![trtexec vs DeepStream total throughput](charts/chart_trt_vs_ds.png)\n\n## 10. Efficiency Analysis\n\n\\`\\`\\`\nDS Efficiency = DS_total_imgs_per_sec / trtexec_imgs_per_sec × 100\nRun 1: ${TOTAL_FPS_RUN1} / ${IMGS_PER_SEC} × 100 = ${EFFICIENCY_RUN1}%\nRun 2: ${TOTAL_FPS_RUN2} / ${IMGS_PER_SEC} × 100 = ${EFFICIENCY_RUN2}%\n\\`\\`\\`\n\nEfficiency gap breakdown: NVDEC decode overhead (~5-10%), GStreamer mux/queue overhead (~5-10%), CPU scheduler jitter (~2-5%).\n\nInterpretation notes for the numbers above:\n\n- **Well-balanced pipeline**: GPU=99-100%, NVDEC=99-100%, CPU=30-40% with no single core pinned. The ~50% DS/trtexec gap at this utilization is physically irreducible — it's the cost of real decode + memory transfers that trtexec skips with \\`--noDataTransfers\\`.\n- **DS efficiency above 100% is expected for ViT / transformer models**: the TRT compiler backend (opt-level 4) often produces bimodal GPU latency with two alternating execution paths (e.g., 1.5ms and 4.0ms modes for OWL-ViT). trtexec reports high variance and a conservative median; DeepStream's pipelined scheduling smooths the bimodal pattern and can achieve 100-110% of the trtexec baseline. This is not a measurement error.\n- **1080p tends to saturate NVDEC** while GPU has headroom. The pipeline is pinned to 720p (\\`sample_720p.mp4\\`) specifically to keep benchmarks comparable across models.\n\n![DeepStream efficiency vs trtexec baseline](charts/chart_efficiency.png)\n\n## 11. Pipeline Timing\n\n| Step | Description | Duration |\n|------|-------------|----------|\n| 1-3 | HF Model Acquire (download + inspect ONNX) | (fill from step timing) |\n| 4 | Engine build | (fill from step timing) |\n| 5 | trtexec BS=1 + BS=${MAX_BS} | (fill from step timing) |\n| 6 | Parser + config + visual validation + KITTI | (fill from step timing) |\n| 7 Run 1 | DS Calibration (${N_RUN1} streams) | (fill from step timing) |\n| 7 Run 2 | DS Validation (${N_RUN2} streams) | (fill from step timing) |\n| 8 | Report generation | (fill) |\n| **Total** | **End-to-end** | **(fill)** |\n\n## 12. Reference Commands\n\n### Engine Build\n\\`\\`\\`bash\ntrtexec --onnx=models/${MODEL_NAME}/model/${MODEL_FILENAME}.onnx \\\\\n --saveEngine=models/${MODEL_NAME}/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine \\\\\n --minShapes=${INPUT_NAME}:1x3x${H}x${W} \\\\\n --optShapes=${INPUT_NAME}:${MAX_BS}x3x${H}x${W} \\\\\n --maxShapes=${INPUT_NAME}:${MAX_BS}x3x${H}x${W} \\\\\n --fp16 --memPoolSize=workspace:32768M \\\\\n --timingCacheFile=models/${MODEL_NAME}/benchmarks/engines/timing.cache\n\\`\\`\\`\n\n### trtexec Benchmark\n\\`\\`\\`bash\n# BS=1\ntrtexec --loadEngine=$(basename $ENGINE) --shapes=${INPUT_NAME}:1x3x${H}x${W} \\\\\n --noDataTransfers --warmUp=1000 --duration=10\n\n# BS=${MAX_BS}\ntrtexec --loadEngine=$(basename $ENGINE) --shapes=${INPUT_NAME}:${MAX_BS}x3x${H}x${W} \\\\\n --noDataTransfers --warmUp=1000 --duration=10\n\\`\\`\\`\n\n### DeepStream Single-Stream Validation\n\\`\\`\\`bash\n# See models/${MODEL_NAME}/scripts/ for full gst-launch-1.0 command\n\\`\\`\\`\n\n### DeepStream Multi-Stream Benchmark\n\\`\\`\\`bash\n# DS Run 1: ${N_RUN1} streams — see models/${MODEL_NAME}/scripts/\n# DS Run 2: ${N_RUN2} streams — see models/${MODEL_NAME}/scripts/\n\\`\\`\\`\n\n### Custom Parser Build\n\\`\\`\\`bash\ncd models/${MODEL_NAME}/parser && make DEEPSTREAM_DIR=/opt/nvidia/deepstream/deepstream CUDA_VER=13.2\n\\`\\`\\`\nMDEOF\necho \"benchmark_report.md written: $(wc -l \u003c models/$MODEL_NAME/reports/benchmark_report.md) lines\"\n```\n\n> **Note on \"fill\" fields**: Fields marked `(fill from ...)` must be replaced with actual values from the step logs before finalizing. Search the step output logs for the exact values and substitute them. Do not leave any `(fill ...)` placeholder in the final report.\n\n## 8c-2 + 8c-3: HTML + PDF Report (MANDATORY — ONE COMMAND)\n\nBefore generating HTML+PDF, verify all 5 charts exist:\n\n```bash\nCHART_DIR=\"models/$MODEL_NAME/reports/charts\"\nMISSING_CHARTS=0\nfor CHART in chart_trtexec_bs1_vs_bsmax.png chart_trtexec_throughput.png \\\n chart_ds_streams_vs_fps.png chart_trt_vs_ds.png chart_efficiency.png; do\n [ ! -f \"$CHART_DIR/$CHART\" ] && { echo \"ERROR: Missing $CHART_DIR/$CHART\"; MISSING_CHARTS=$((MISSING_CHARTS+1)); }\ndone\n[ \"$MISSING_CHARTS\" -gt 0 ] && { echo \"ERROR: $MISSING_CHARTS chart(s) missing — re-run 8c-1\"; exit 1; }\necho \"All 5 charts verified OK\"\n```\n\nThen run the canonical pipeline script — this generates BOTH the HTML and PDF correctly:\n\n```bash\n# setup.sh apt-installs wkhtmltopdf INSIDE the container, so it does NOT survive the ephemeral\n# `--rm` bootstrap container. Every later `docker run --rm` starts without it and the PDF step\n# silently degrades to HTML-only. Ensure it is present in THIS invocation before rendering.\ncommand -v wkhtmltopdf >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq wkhtmltopdf; }\n\npython3 .claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py \\\n models/$MODEL_NAME/reports/benchmark_report.md \\\n .claude/skills/deepstream-import-vision-model/scripts/report/report-style.css \\\n models/$MODEL_NAME/reports/ \\\n $MODEL_NAME\n```\n\nThis script uses `report_style.css` (navy `#283593` headers, `#e8eaf6` rows, `#263238` code blocks), embeds charts as base64 data URIs, calls `wkhtmltopdf` internally, and outputs `benchmark_report.html` + `benchmark_report_{model_name}.pdf`.\n\n> **NAMING RULES:**\n> - HTML: always `benchmark_report.html` (no model name suffix)\n> - PDF: always `benchmark_report_{model_name}.pdf` (model name postfix required)\n\nVerify PDF size is >500 KB (confirms charts embedded). Run all python commands with the shared venv active (`source build/.venv_optimum/bin/activate`); `markdown` and `matplotlib` are already installed there.\n\n## 8c-4: Final Report Checklist and Timing\n\nAfter generating markdown, HTML, and PDF, record step timing:\n\n```bash\nSTEP8_END=$(date +%s.%N)\nSTEP8_DURATION=$(python3 -c \"print(round($STEP8_END - $STEP8_START, 2))\") # bc is not in the container; python3 always is\necho \"[Step 8] Report generation completed in ${STEP8_DURATION}s\"\n```\n\nBefore marking the report as complete, verify ALL of these exist:\n- [ ] `reports/benchmark_report.md` — markdown source (12 sections)\n- [ ] `reports/benchmark_report.html` — styled HTML (charts/ alongside)\n- [ ] `reports/benchmark_report_{model_name}.pdf` — PDF >500 KB (confirms charts embedded)\n- [ ] `reports/benchmark_data.json` — raw benchmark numbers\n- [ ] `reports/charts/` — all 5 PNGs: `chart_trtexec_bs1_vs_bsmax.png`, `chart_trtexec_throughput.png`, `chart_ds_streams_vs_fps.png`, `chart_trt_vs_ds.png`, `chart_efficiency.png`\n- **Charts**: fixed filenames above — never rename or add model name suffix to charts\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":27412,"content_sha256":"4dce97e78d5221b20a2002eb87a6081fd3346012bc996350c24a4ce58337a0d6"},{"filename":"references/windows.md","content":"\u003c!--\nCopyright (c) 2026, NVIDIA CORPORATION. All rights reserved.\nLicensed under the Apache License, Version 2.0 (the \"License\").\n-->\n\n# Running on Windows (and cross-platform)\n\nThis skill runs on **Windows with Docker Desktop** with **no host packages**: every compute step runs\n**inside the DeepStream Linux container** via `docker run`. The host only needs `docker` + the NVIDIA\ndriver. The single exception is the host-side install (copying the skill into `\u003cproject>\\.claude\\skills\\`),\nwhich cannot run in a container — so the skill ships exactly one PowerShell script, **`install.ps1`**,\nthe twin of `install.sh`. There are no `.ps1` duplicates of the compute scripts.\n\n## Why this works\nThe ONNX export, TensorRT engine build, custom nvinfer parser compile, DeepStream run, and PDF\nreport all execute **inside** `nvcr.io/nvidia/deepstream:9.1-triton-multiarch` (the venv\n`build/.venv_optimum` + `wkhtmltopdf` are installed into that container by `setup.sh`). The container\nis the portability layer. The only per-shell difference is the `docker run` **bind-mount token**.\n\n## Prerequisites (Windows)\n1. **Docker Desktop** with the **WSL2 backend enabled** (Settings → General → *Use the WSL 2 based\n engine*) — required for GPU (`--gpus all` works only through the WSL2 backend).\n2. A recent **NVIDIA driver** with WSL/CUDA support. No CUDA toolkit / TensorRT / DeepStream needed\n on the host — the container ships them.\n3. Docker Desktop → Settings → Resources → **File Sharing**: share the drive holding your working dir.\n4. `docker pull nvcr.io/nvidia/deepstream:9.1-triton-multiarch`.\n\n## The one thing that differs per shell: the mount token\nClaude Code fills this in based on the host OS:\n\n| Shell | working-dir mount |\n|-------|-------------------|\n| **PowerShell** | `-v \"${PWD}:/work\"` |\n| **cmd** | `-v \"%cd%:/work\"` |\n| **WSL2 / Linux bash** | `-v \"$PWD\":/work` |\n\n## Bootstrap + preflight (PowerShell example)\n```powershell\n# one-time bootstrap: venv + torch/onnx/onnxruntime + wkhtmltopdf, all in-container\ndocker run --rm -it --gpus all --shm-size=16g -v \"${PWD}:/work\" -w /work `\n --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch `\n .claude/skills/deepstream-import-vision-model/setup.sh\n\n# preflight — GPU + venv + trtexec (container-mode auto-detects /.dockerenv)\ndocker run --rm --gpus all -v \"${PWD}:/work\" -w /work `\n --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch `\n .claude/skills/deepstream-import-vision-model/scripts/preflight.sh\n```\nEvery subsequent phase runs the same way — via `docker run … -lc '\u003ccommands>'` or the\n`.claude/skills/deepstream-import-vision-model/scripts/dsrun.sh` wrapper.\n\n## Notes\n- The skill ships a `.gitattributes` forcing **LF** on all scripts, so a Windows checkout won't\n CRLF-corrupt them (CRLF breaks bash-in-container).\n- `--shm-size=16g` works on the WSL2 backend.\n- **Install:** on native Windows run the bundled **`install.ps1`** — the twin of `install.sh`, same\n sequence and flags (`-Target`=`--target`, `-NoCursor`=`--no-cursor`, `-DryRun`=`--dry-run`):\n `.\\install.ps1 -Target C:\\path\\to\\project` (copies the skill into `\u003cproject>\\.claude\\skills\\`). On\n Linux/WSL2/Git Bash use `bash install.sh --target \u003cproject>`.\n- Prefer a **WSL2 Ubuntu terminal** for the exact Linux experience — inside WSL2 everything runs\n unchanged.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3368,"content_sha256":"a417b766068fe8a3db7a58168b9b5890eb1f5ec96d24bf7bef8588356cc6512e"},{"filename":"scripts/deepstream/benchmark-ds.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -euo pipefail\n################################################################################\n# DeepStream benchmark using gst-launch-1.0\n# Thumb rule: batch_size == num_streams (always equal).\n# Measures total throughput by timing full video processing with fakesink.\n#\n# Usage: ./benchmark-ds.sh \u003cconfig_file> \u003cnum_streams> [input_video]\n# Example: ./benchmark-ds.sh config_infer_primary_b21.txt 21 video.mp4\n#\n# batch_size in the nvinfer config must match num_streams.\n################################################################################\n\nCONFIG=\"${1:-}\"\nNUM_STREAMS=\"${2:-}\"\nVIDEO=\"${3:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}\"\nMUXER_W=1280\nMUXER_H=720\nNS_PER_SEC=$(( 1000 * 1000 * 1000 ))\n\nif [ -z \"$CONFIG\" ] || [ -z \"$NUM_STREAMS\" ]; then\n echo \"Usage: $0 \u003cconfig_file> \u003cnum_streams> [input_video]\"\n exit 1\nfi\n\n# Detect video FPS via mediainfo; fall back to 30 for the standard sample\nVIDEO_FPS=$(mediainfo --Inform=\"Video;%FrameRate%\" \"${VIDEO}\" 2>/dev/null | awk '{printf \"%.0f\", $1+0}')\nVIDEO_FPS=\"${VIDEO_FPS:-30}\"\n\n# Detect actual frame count; fall back to 1440 if mediainfo unavailable or fails\nif [ -n \"$3\" ]; then\n FRAMES_PER_STREAM=$(mediainfo --Inform=\"Video;%FrameCount%\" \"${VIDEO}\" 2>/dev/null)\n if ! echo \"$FRAMES_PER_STREAM\" | grep -qE '^[0-9]+

DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…

|| [ \"$FRAMES_PER_STREAM\" -eq 0 ]; then\n echo \"Warning: mediainfo failed, falling back to 1440 frames\" >&2\n FRAMES_PER_STREAM=1440\n fi\nelse\n # Default sample_720p.mp4 is ~1440 frames at 30fps\n FRAMES_PER_STREAM=1440\nfi\nTOTAL_FRAMES=$((FRAMES_PER_STREAM * NUM_STREAMS))\n\necho \"=== DeepStream Benchmark ===\"\necho \"Config: $CONFIG\"\necho \"Streams: $NUM_STREAMS\"\necho \"Frames/stream: $FRAMES_PER_STREAM\"\necho \"Total frames: $TOTAL_FRAMES\"\necho \"\"\n\n# Build source elements\nSOURCES=\"\"\nfor i in $(seq 0 $((NUM_STREAMS - 1))); do\n SOURCES+=\"filesrc location=${VIDEO} ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! mux.sink_${i} \"\ndone\n\nPIPELINE=\"${SOURCES} nvstreammux name=mux batch-size=${NUM_STREAMS} width=${MUXER_W} height=${MUXER_H} batched-push-timeout=-1 ! \\\n queue ! nvinfer config-file-path=${CONFIG} ! queue ! fakesink sync=0\"\n\necho \"Starting pipeline...\"\nSTART_TIME=$(date +%s%N)\n\nGST_DEBUG=0 gst-launch-1.0 -e ${PIPELINE} 2>&1 | grep -v \"^$\" || true\n\nEND_TIME=$(date +%s%N)\nELAPSED_NS=$((END_TIME - START_TIME))\n# awk instead of bc (bc is not installed in the DeepStream container; awk always is)\nELAPSED_SEC=$(awk \"BEGIN{printf \\\"%.2f\\\", $ELAPSED_NS / $NS_PER_SEC}\")\nFPS=$(awk \"BEGIN{printf \\\"%.1f\\\", $TOTAL_FRAMES / $ELAPSED_SEC}\")\nREALTIME=$(awk \"BEGIN{printf \\\"%.2f\\\", $FPS / (${NUM_STREAMS} * ${VIDEO_FPS})}\")\n\necho \"\"\necho \"=== Results ===\"\necho \"Wall time: ${ELAPSED_SEC}s\"\necho \"Total frames: ${TOTAL_FRAMES}\"\necho \"Throughput: ${FPS} img/s\"\necho \"Per-stream: $(awk \"BEGIN{printf \\\"%.1f\\\", $FPS / $NUM_STREAMS}\") fps\"\necho \"Real-time factor: ${REALTIME}x (${NUM_STREAMS} streams @ ${VIDEO_FPS}fps)\"\necho \"===============\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3718,"content_sha256":"ac036abb43ad706bc8428617dcb5b88b186c2118643853442aecec60622eec01"},{"filename":"scripts/deepstream/ds-kitti-dump.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n################################################################################\n# Step 6: KITTI dump using deepstream-app (built-in KITTI support)\n# Generates a temporary deepstream-app config, runs for N frames, dumps KITTI.\n#\n# Usage: ./ds-kitti-dump.sh \u003cnvinfer_config> \u003ckitti_output_dir> [num_frames] [input_video]\n# Example: ./ds-kitti-dump.sh config_infer_primary_yolox.txt kitti_output 100\n################################################################################\nset -euo pipefail\n\nNVINFER_CONFIG=\"$1\"\nKITTI_DIR=\"$2\"\nNUM_FRAMES=\"${3:-100}\"\nVIDEO=\"${4:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}\"\n\nif [ -z \"$NVINFER_CONFIG\" ] || [ -z \"$KITTI_DIR\" ]; then\n echo \"Usage: $0 \u003cnvinfer_config> \u003ckitti_output_dir> [num_frames] [input_video]\"\n exit 1\nfi\n\n# Validate inputs before resolving paths\n[ -f \"$NVINFER_CONFIG\" ] || { echo \"ERROR: nvinfer config not found: $NVINFER_CONFIG\"; exit 1; }\n[ -f \"$VIDEO\" ] || { echo \"ERROR: video file not found: $VIDEO\"; exit 1; }\n\n# Resolve to absolute paths\nNVINFER_CONFIG=\"$(realpath \"$NVINFER_CONFIG\")\"\nKITTI_DIR=\"$(realpath -m \"$KITTI_DIR\")\"\nVIDEO=\"$(realpath \"$VIDEO\")\"\n\nmkdir -p \"${KITTI_DIR}\"\n\necho \"=== DeepStream KITTI Dump ===\"\necho \"nvinfer config: $NVINFER_CONFIG\"\necho \"KITTI dir: $KITTI_DIR\"\necho \"Max frames: $NUM_FRAMES\"\necho \"Input video: $VIDEO\"\necho \"\"\n\n# Generate temporary deepstream-app config\ntrap 'rm -f \"${TMPCONFIG:-}\"' EXIT\nTMPCONFIG=$(mktemp /tmp/ds_kitti_XXXXXX.txt)\n\ncat > \"$TMPCONFIG\" \u003c\u003c EOF\n[application]\nenable-perf-measurement=0\ngie-kitti-output-dir=${KITTI_DIR}\n\n[tiled-display]\nenable=0\n\n[source0]\nenable=1\ntype=3\nuri=file://${VIDEO}\nnum-sources=1\ngpu-id=0\n\n[sink0]\nenable=1\ntype=1\n#1=FakeSink\nsync=0\n\n[osd]\nenable=0\n\n[streammux]\nlive-source=0\nbatch-size=1\nbatched-push-timeout=-1\nwidth=1280\nheight=720\n\n[primary-gie]\nenable=1\nbatch-size=1\ngie-unique-id=1\nconfig-file=${NVINFER_CONFIG}\n\n[tests]\nfile-loop=0\nEOF\n\necho \"Temp config: $TMPCONFIG\"\necho \"Running deepstream-app...\"\n\n# Run deepstream-app (it will process entire video).\n# Temporarily disable pipefail so head -30 closing the pipe early (SIGPIPE to grep)\n# doesn't trigger set -e before we can capture deepstream-app's exit code.\nset +o pipefail\ntimeout 120 deepstream-app -c \"$TMPCONFIG\" 2>&1 | grep -v \"^$\" | head -30\nDS_EXIT_CODE=${PIPESTATUS[0]}\nset -o pipefail\n\nif [ $DS_EXIT_CODE -eq 124 ]; then\n echo \"Warning: deepstream-app timed out after 120 seconds\"\nelif [ $DS_EXIT_CODE -ne 0 ]; then\n echo \"Error: deepstream-app failed with exit code $DS_EXIT_CODE\"\n exit 1\nfi\n\n# Count KITTI files generated\nTOTAL_FILES=$(ls -1 \"${KITTI_DIR}\"/*.txt 2>/dev/null | wc -l)\necho \"\"\necho \"Total KITTI files generated: ${TOTAL_FILES}\"\n\n# Keep only first N frames, remove the rest\nif [ \"$TOTAL_FILES\" -gt \"$NUM_FRAMES\" ]; then\n # Guard against misconfigured KITTI_DIR blowing away something else\n [ -n \"$KITTI_DIR\" ] && [ -d \"$KITTI_DIR\" ] && [ \"$KITTI_DIR\" != \"/\" ] \\\n || { echo \"ERROR: invalid KITTI_DIR for cleanup: $KITTI_DIR\"; exit 1; }\n TO_REMOVE=$((TOTAL_FILES - NUM_FRAMES))\n echo \"Trimming to first ${NUM_FRAMES} frames (removing ${TO_REMOVE})...\"\n # NUL-delimited read so filenames with spaces/newlines are handled safely.\n KITTI_FILES=()\n while IFS= read -r -d '' f; do\n KITTI_FILES+=(\"$f\")\n done \u003c \u003c(find \"$KITTI_DIR\" -maxdepth 1 -type f -name '*.txt' -print0 | sort -z)\n for ((i = NUM_FRAMES; i \u003c ${#KITTI_FILES[@]}; i++)); do\n rm -f -- \"${KITTI_FILES[i]}\"\n done\n TOTAL_FILES=$(find \"$KITTI_DIR\" -maxdepth 1 -type f -name '*.txt' 2>/dev/null | wc -l)\n echo \"Kept ${TOTAL_FILES} KITTI files\"\nfi\n\n# Show sample KITTI output\necho \"\"\necho \"=== Sample KITTI Output (first 3 files) ===\"\nfor f in $(ls -1 \"${KITTI_DIR}\"/*.txt 2>/dev/null | sort | head -3); do\n echo \"--- $(basename $f) ---\"\n cat \"$f\"\ndone\n\necho \"\"\necho \"=== KITTI Dump Complete ===\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4602,"content_sha256":"cd1765a6ab609e2ff7d9ec8111a33dba3eff107382695b22754094d64363f153"},{"filename":"scripts/deepstream/ds-perf-run.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n################################################################################\n# Step 7c: DeepStream perf-measurement run via deepstream-app.\n#\n# Replaces the older `gst-launch-1.0 ... ! fpsdisplaysink ...` benchmark, which\n# pulled in `gstreamer1.0-plugins-bad`. `deepstream-app` is part of the NVIDIA\n# DeepStream SDK and emits `**PERF: fps_run0 (fps_avg0) fps_run1 (fps_avg1) ...`\n# lines (one pair per active source) that the report-generation phase parses.\n#\n# Usage: ./ds-perf-run.sh \u003cnvinfer_config> \u003cnum_streams> \u003clog_path> [input_video]\n# Example:\n# ./ds-perf-run.sh config_infer_ds_yolox.txt 32 \\\n# models/yolox/benchmarks/ds/ds_s32_run1.log \\\n# /opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4\n#\n# Notes:\n# - `[primary-gie] batch-size` and `[streammux] batch-size` are both set to N\n# (matches the skill-wide rule batch_size == num_streams).\n# - `num-sources=N` fans the single input video out to N pipeline sources;\n# deepstream-app handles the file-loop / EOS bookkeeping.\n# - The nvinfer config must already point at the engine, parser, and labels.\n# This script does NOT mutate the nvinfer config.\n################################################################################\nset -euo pipefail\n\nNVINFER_CONFIG=\"${1:-}\"\nNUM_STREAMS=\"${2:-}\"\nLOG_PATH=\"${3:-}\"\nVIDEO=\"${4:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}\"\n\nif [ -z \"$NVINFER_CONFIG\" ] || [ -z \"$NUM_STREAMS\" ] || [ -z \"$LOG_PATH\" ]; then\n echo \"Usage: $0 \u003cnvinfer_config> \u003cnum_streams> \u003clog_path> [input_video]\"\n exit 1\nfi\n\n[ -f \"$NVINFER_CONFIG\" ] || { echo \"ERROR: nvinfer config not found: $NVINFER_CONFIG\"; exit 1; }\n[ -f \"$VIDEO\" ] || { echo \"ERROR: video file not found: $VIDEO\"; exit 1; }\ncommand -v deepstream-app >/dev/null 2>&1 || { echo \"ERROR: deepstream-app not on PATH\"; exit 1; }\n\nNVINFER_CONFIG=\"$(realpath \"$NVINFER_CONFIG\")\"\nVIDEO=\"$(realpath \"$VIDEO\")\"\nLOG_PATH=\"$(realpath -m \"$LOG_PATH\")\"\nmkdir -p \"$(dirname \"$LOG_PATH\")\"\n\nN=\"$NUM_STREAMS\"\nMUXER_W=1280\nMUXER_H=720\n\necho \"=== DeepStream Perf Run ===\"\necho \"nvinfer config: $NVINFER_CONFIG\"\necho \"Streams (=N): $N\"\necho \"Input video: $VIDEO\"\necho \"Log path: $LOG_PATH\"\necho \"\"\n\ntrap 'rm -f \"${TMPCONFIG:-}\"' EXIT\nTMPCONFIG=$(mktemp /tmp/ds_perf_XXXXXX.txt)\n\ncat > \"$TMPCONFIG\" \u003c\u003cEOF\n[application]\nenable-perf-measurement=1\nperf-measurement-interval-sec=2\n\n[tiled-display]\nenable=0\n\n[source0]\nenable=1\ntype=3\nuri=file://${VIDEO}\nnum-sources=${N}\ngpu-id=0\n\n[sink0]\nenable=1\ntype=1\nsync=0\n\n[osd]\nenable=0\n\n[streammux]\nlive-source=0\nbatch-size=${N}\nbatched-push-timeout=-1\nwidth=${MUXER_W}\nheight=${MUXER_H}\n\n[primary-gie]\nenable=1\nbatch-size=${N}\ngie-unique-id=1\nconfig-file=${NVINFER_CONFIG}\n\n[tests]\nfile-loop=1\nEOF\n\necho \"Temp config: $TMPCONFIG\"\necho \"Running deepstream-app...\"\n\nset +o pipefail\n# file-loop=1 has no built-in stop condition; timeout(1) kills deepstream-app\n# after 60 s and returns exit 124 — treated as success below.\ntimeout 60s deepstream-app -c \"$TMPCONFIG\" 2>&1 | tee \"$LOG_PATH\"\nDS_EXIT_CODE=${PIPESTATUS[0]}\nset -o pipefail\n\n# exit 124 = timeout fired as expected (file-loop=1, 60 s cap)\nif [ $DS_EXIT_CODE -ne 0 ] && [ $DS_EXIT_CODE -ne 124 ]; then\n echo \"ERROR: deepstream-app exited with code $DS_EXIT_CODE — see $LOG_PATH\" >&2\n exit \"$DS_EXIT_CODE\"\nfi\n\n# Average stream-0 instantaneous FPS across the last 10 **PERF: lines.\n# Using stream 0 (the \\K capture after `**PERF:`) gives exactly 1 value per\n# measurement window so tail -10 always covers 10 windows regardless of N.\n# Multiply by N for total throughput.\nPERF_FPS=$(grep -oP '\\*\\*PERF:\\s*\\K[0-9.]+' \"$LOG_PATH\" | tail -10 | python3 -c \"\nimport sys\nvals = [float(line) for line in sys.stdin if line.strip()]\nprint(round(sum(vals)/len(vals), 2) if vals else 0)\n\")\n\nif [ -z \"$PERF_FPS\" ] || [ \"$PERF_FPS\" = \"0\" ]; then\n echo \"ERROR: no **PERF: lines parsed from $LOG_PATH\" >&2\n exit 1\nfi\n\nTOTAL_FPS=$(python3 -c \"print(round(float('$PERF_FPS') * $N, 2))\")\n\necho \"\"\necho \"=== Perf Summary ===\"\necho \"Streams: $N\"\necho \"FPS/stream: $PERF_FPS\"\necho \"Total imgs/sec: $TOTAL_FPS\"\necho \"Log: $LOG_PATH\"\necho \"====================\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4889,"content_sha256":"d15f1ddfb833be7f99e81e08f93dafcc3301a1de86827f651db500ccde88f1ce"},{"filename":"scripts/deepstream/ds-single-stream.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n################################################################################\n# Step 6: Single-stream DeepStream pipeline -- saves output video with OSD boxes.\n#\n# Usage: ./ds-single-stream.sh \u003cconfig_file> \u003coutput_video> [input_video]\n# Example: ./ds-single-stream.sh config_infer_primary_yolox.txt yolox_output.mp4\n#\n# Encoder policy (MANDATORY):\n# - Primary path uses nvv4l2h264enc (NVENC) with .mp4 container. nvdsosd\n# overlays are reliably preserved only with NVENC on the NVMM memory path.\n# - x264enc and openh264enc are PROHIBITED and must never be used.\n# - On NVENC-init failure, the script checks theoraenc + oggmux availability\n# (LGPL elements; both ship in gst-plugins-base):\n# * Available → falls back to theoraenc+oggmux → saves \u003coutput>.ogv\n# nvvideoconvert ! \"video/x-raw, format=I420\" ! theoraenc quality=48 ! oggmux\n# Emits DS_SINGLE_STREAM_MODE=theoraenc-fallback and DS_SINGLE_STREAM_OUTPUT=\u003cpath>\n# * Unavailable → skips video creation, emits DS_SINGLE_STREAM_MODE=skipped, exit 0\n# The benchmark report must surface which encoder mode was used.\n################################################################################\n\nset -o pipefail\n\nCONFIG=\"$1\"\nOUTPUT=\"$2\"\nVIDEO=\"${3:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}\"\nMUXER_W=1280\nMUXER_H=720\n\nif [ -z \"$CONFIG\" ] || [ -z \"$OUTPUT\" ]; then\n echo \"Usage: $0 \u003cconfig_file> \u003coutput_video> [input_video]\"\n exit 1\nfi\n\nOUTPUT_DIR=\"$(dirname \"$OUTPUT\")\"\nLOG_FILE=\"$(mktemp -t ds-single-stream-XXXXXX.log)\"\ntrap 'rm -f \"$LOG_FILE\"' EXIT\n\nmkdir -p \"$OUTPUT_DIR\"\n\necho \"=== DeepStream Single-Stream Test ===\"\necho \"Config: $CONFIG\"\necho \"Input: $VIDEO\"\necho \"Output: $OUTPUT (primary: nvv4l2h264enc)\"\necho \"\"\n\ngst-launch-1.0 \\\n filesrc location=\"${VIDEO}\" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! mux.sink_0 \\\n nvstreammux name=mux batch-size=1 width=${MUXER_W} height=${MUXER_H} batched-push-timeout=-1 ! \\\n nvinfer config-file-path=\"${CONFIG}\" ! \\\n nvvideoconvert ! nvdsosd ! nvvideoconvert ! \\\n \"video/x-raw(memory:NVMM), format=NV12\" ! nvv4l2h264enc ! h264parse ! mp4mux ! \\\n filesink location=\"${OUTPUT}\" sync=0 \\\n 2>&1 | tee \"$LOG_FILE\"\nSTATUS=${PIPESTATUS[0]}\n\nif [ $STATUS -eq 0 ] && [ -s \"$OUTPUT\" ]; then\n echo \"\"\n echo \"Output saved to: ${OUTPUT}\"\n echo \"DS_SINGLE_STREAM_MODE=nvenc-primary\"\n echo \"DS_SINGLE_STREAM_OUTPUT=${OUTPUT}\"\n exit 0\nfi\n\n# Detect NVENC-init failure -- the only condition under which we use the theoraenc fallback.\n# x264enc and openh264enc are prohibited. Any other failure surfaces as a hard error.\nif grep -qE \"v4l2-nvenc.*failed during initialization|Could not open device.*v4l2-nvenc|nvv4l2h264enc.*not-negotiated\" \"$LOG_FILE\"; then\n echo \"\"\n echo \"WARNING: nvv4l2h264enc (NVENC) is unavailable on this GPU.\" >&2\n\n if ! gst-inspect-1.0 theoraenc > /dev/null 2>&1 || ! gst-inspect-1.0 oggmux > /dev/null 2>&1; then\n echo \"WARNING: theoraenc/oggmux not available. Skipping video creation.\" >&2\n echo \"DS_SINGLE_STREAM_MODE=skipped\"\n exit 0\n fi\n\n echo \" Falling back to theoraenc+oggmux (OGV output).\" >&2\n echo \"\"\n OGV_OUTPUT=\"$(echo \"${OUTPUT}\" | sed -E 's/\\.[Mm][Pp]4$//').ogv\"\n rm -f \"$OUTPUT\" \"$OGV_OUTPUT\"\n\n gst-launch-1.0 \\\n filesrc location=\"${VIDEO}\" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! mux.sink_0 \\\n nvstreammux name=mux batch-size=1 width=${MUXER_W} height=${MUXER_H} batched-push-timeout=-1 ! \\\n nvinfer config-file-path=\"${CONFIG}\" ! \\\n nvvideoconvert ! nvdsosd ! nvvideoconvert ! \\\n \"video/x-raw, format=I420\" ! theoraenc quality=48 ! oggmux ! \\\n filesink location=\"${OGV_OUTPUT}\" sync=0 \\\n 2>&1\n THEORA_STATUS=$?\n\n if [ $THEORA_STATUS -eq 0 ] && [ -s \"$OGV_OUTPUT\" ]; then\n echo \"\"\n echo \"theoraenc fallback succeeded. Output saved to: ${OGV_OUTPUT}\"\n echo \"DS_SINGLE_STREAM_MODE=theoraenc-fallback\"\n echo \"DS_SINGLE_STREAM_OUTPUT=${OGV_OUTPUT}\"\n exit 0\n fi\n\n echo \"ERROR: theoraenc fallback pipeline failed (exit ${THEORA_STATUS}).\" >&2\n exit ${THEORA_STATUS:-1}\nfi\n\necho \"Pipeline failed with exit code $STATUS (not an NVENC-init failure).\" >&2\nexit $STATUS\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5015,"content_sha256":"6b0e0e760dd07e4d2725422ef50f1eb271747b49f13c7842e3f11db46469ac5c"},{"filename":"scripts/deepstream/ds-sweep.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n################################################################################\n# DeepStream BS_OPT sweep — smart 2-phase approach.\n#\n# Phase 1: trtexec probe at BS=1,4,8 (~30s total, fast)\n# - Fits power-law curve: QPS = a × BS^(-alpha)\n# - Predicts BS where trtexec QPS = FPS_THRESHOLD / DS_EFFICIENCY\n# - This accounts for DeepStream pipeline overhead vs raw trtexec\n#\n# Phase 2: DeepStream confirmation (1-2 runs)\n# - Runs DS at BS_pred and BS_pred-step if needed\n# - Picks highest BS where DS fps/stream >= FPS_THRESHOLD\n# - Uses dynamic engine (no per-BS engine builds during sweep)\n#\n# Thumb rules:\n# - batch_size == num_streams (always equal)\n# - Dynamic engine: min=1, opt=10, max=max(BATCH_SIZES_PROBE) e.g. 8\n# Extended at build time to max=BS_pred+margin once predicted\n# - BS_OPT drives production engine build (static, timing cache reuse)\n#\n# Usage:\n# ./ds-sweep.sh \u003cdynamic_engine> \u003connx_path> \u003cconfig_template> \\\n# \u003cparser_so> \u003clabels> \u003cengines_dir> \u003cconfigs_dir> [video]\n################################################################################\nset -euo pipefail\n\nDYNAMIC_ENGINE=\"$1\"\nONNX_PATH=\"$2\"\nCONFIG_TEMPLATE=\"$3\"\nPARSER_SO=\"$4\"\nLABELS=\"$5\"\nENGINES_DIR=\"$6\"\nCONFIGS_DIR=\"$7\"\nVIDEO=\"${8:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}\"\n\n# Derive INPUT_NAME, H, W from the ONNX model — mirrors how engine-build.md does it.\n# Env var overrides let callers handle models with dynamic spatial dims (e.g. H=800 W=800 ./ds-sweep.sh ...).\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nINSPECT_SCRIPT=\"$(realpath \"${SCRIPT_DIR}/../../model/inspect-onnx.py\")\"\nif [ -z \"${INPUT_NAME:-}\" ] || [ -z \"${H:-}\" ] || [ -z \"${W:-}\" ]; then\n INSPECT_OUT=$(python3 \"${INSPECT_SCRIPT}\" \"${ONNX_PATH}\")\n INPUT_NAME=\"${INPUT_NAME:-$(echo \"${INSPECT_OUT}\" | grep -oP 'input_name:\\s*\\K\\S+')}\"\n H=\"${H:-$(echo \"${INSPECT_OUT}\" | grep -oP 'height:\\s*\\K[0-9]+')}\"\n W=\"${W:-$(echo \"${INSPECT_OUT}\" | grep -oP 'width:\\s*\\K[0-9]+')}\"\nfi\n[ -z \"${INPUT_NAME}\" ] && { echo \"ERROR: could not parse INPUT_NAME from ONNX — set INPUT_NAME env var\"; exit 1; }\n[ -z \"${H}\" ] && { echo \"ERROR: H not detected (dynamic spatial dims) — set H env var, e.g. H=800\"; exit 1; }\n[ -z \"${W}\" ] && { echo \"ERROR: W not detected (dynamic spatial dims) — set W env var, e.g. W=800\"; exit 1; }\n\n# DS_ERR_LOG: destination for GStreamer/DeepStream stderr output.\n# Override via environment variable to redirect elsewhere (e.g. a file path or /dev/stderr).\n# Defaults to a log file alongside the sweep engine logs so errors are preserved for diagnosis.\nDS_ERR_LOG=\"${DS_ERR_LOG:-${ENGINES_DIR}/ds_sweep_gst_errors.log}\"\nmkdir -p \"$(dirname \"${DS_ERR_LOG}\")\"\n# Truncate/create the log at the start of the sweep so it reflects the current run only.\n: > \"${DS_ERR_LOG}\"\necho \"[ds-sweep] GStreamer stderr → ${DS_ERR_LOG}\"\n\n# TIMING_CACHE=\"${ENGINES_DIR}/timing.cache\" # used by the engine-build phase, not sweep\nNS_PER_SEC=$(( 1000 * 1000 * 1000 )) # nanoseconds per second (date +%s%N divisor)\nFPS_THRESHOLD=30 # target fps/stream in DeepStream\nDS_EFFICIENCY=0.65 # DS is ~65% of trtexec throughput (GStreamer pipeline overhead\n # includes muxer, memory mgmt, custom parser, metadata — measured)\nTRT_QPS_TARGET=$(awk \"BEGIN{printf \\\"%.4f\\\", ${FPS_THRESHOLD} / ${DS_EFFICIENCY}}\") # ~46.2 QPS (awk: bc absent in container)\nPROBE_SIZES=(1 4 8) # fast trtexec probe batch sizes\nPROBE_DURATION=10 # seconds per trtexec probe run\n# NEVER use filesrc num-buffers as a frame count — num-buffers counts file byte blocks (4096B),\n# not video frames. Leave num-buffers unset so filesrc reads to natural EOS.\n# Detect actual frame count and FPS via mediainfo — consistent with benchmark-ds.sh.\nVIDEO_FPS=$(mediainfo --Inform=\"Video;%FrameRate%\" \"${VIDEO}\" 2>/dev/null | awk '{printf \"%.0f\", $1+0}')\nVIDEO_FPS=\"${VIDEO_FPS:-30}\"\nACTUAL_FRAMES_PER_STREAM=$(mediainfo --Inform=\"Video;%FrameCount%\" \"${VIDEO}\" 2>/dev/null)\nif ! echo \"${ACTUAL_FRAMES_PER_STREAM}\" | grep -qE '^[0-9]+

DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…

|| [ \"${ACTUAL_FRAMES_PER_STREAM:-0}\" -eq 0 ]; then\n ACTUAL_FRAMES_PER_STREAM=1440 # fallback for sample_720p.mp4: ~48s × 30fps\nfi\necho \" Video frames/stream: ${ACTUAL_FRAMES_PER_STREAM} (${VIDEO_FPS}fps detected)\"\nMUXER_W=1280\nMUXER_H=720\n\nmkdir -p \"${CONFIGS_DIR}\"\n\necho \"======================================================\"\necho \"DS BS_OPT Sweep — 2-Phase Smart Search\"\necho \" FPS threshold : ${FPS_THRESHOLD} fps/stream\"\necho \" DS efficiency : ${DS_EFFICIENCY} (trtexec QPS target: ${TRT_QPS_TARGET})\"\necho \" Probe sizes : ${PROBE_SIZES[*]}\"\necho \" Input tensor : ${INPUT_NAME} (${H}x${W})\"\necho \"======================================================\"\n\n# ── PHASE 1: trtexec probe at BS=1,4,8 ──────────────────\necho \"\"\necho \"PHASE 1: trtexec probe (BS=${PROBE_SIZES[*]})\"\n\ndeclare -a PROBE_BS_ARR PROBE_QPS_ARR\n\nfor BS in \"${PROBE_SIZES[@]}\"; do\n echo \" trtexec BS=${BS}...\"\n LOG=\"${ENGINES_DIR}/probe_bs${BS}.log\"\n trtexec \\\n --loadEngine=\"${DYNAMIC_ENGINE}\" \\\n --fp16 \\\n --shapes=${INPUT_NAME}:${BS}x3x${H}x${W} \\\n --duration=${PROBE_DURATION} \\\n --warmUp=2000 \\\n > \"${LOG}\" 2>&1\n QPS=$(grep \"Throughput:\" \"${LOG}\" | grep -oP 'Throughput: \\K[0-9.]+' | head -1)\n echo \" BS=${BS}: ${QPS} QPS\"\n PROBE_BS_ARR+=(\"${BS}\")\n PROBE_QPS_ARR+=(\"${QPS}\")\ndone\n\n# ── Power-law fit: QPS = a × BS^(-alpha) ────────────────\n# Use BS=4 and BS=8 points to fit alpha (most stable region)\n# alpha = log(QPS4/QPS8) / log(8/4)\nQPS4=\"${PROBE_QPS_ARR[1]}\"\nQPS8=\"${PROBE_QPS_ARR[2]}\"\n\nALPHA=$(python3 -c \"\nimport math\nqps4, qps8 = float('${QPS4}'), float('${QPS8}')\nalpha = math.log(qps4 / qps8) / math.log(8.0 / 4.0)\nprint(f'{alpha:.4f}')\n\")\nA_COEFF=$(python3 -c \"\nimport math\nqps8, alpha = float('${QPS8}'), float('${ALPHA}')\na = qps8 * (8.0 ** alpha)\nprint(f'{a:.4f}')\n\")\n\necho \"\"\necho \" Curve fit: QPS = ${A_COEFF} × BS^(-${ALPHA})\"\n\n# Solve for BS where QPS = TRT_QPS_TARGET\n# BS_pred = (a / QPS_target)^(1/alpha)\n# Guard: if alpha ~ 0 (flat curve — memory-bandwidth-bound or very small model),\n# 1/alpha diverges. Use the cap directly and let Phase 2 DS runs confirm.\nBS_PRED=$(python3 -c \"\nimport math\na, alpha = float('${A_COEFF}'), float('${ALPHA}')\ntarget = float('${TRT_QPS_TARGET}')\nif abs(alpha) \u003c 1e-3:\n bs_pred = 128\nelse:\n bs_pred = (a / target) ** (1.0 / alpha)\nprint(int(bs_pred))\n\")\n\necho \" Predicted BS_pred = ${BS_PRED} (trtexec QPS ≈ ${TRT_QPS_TARGET} at this batch)\"\necho \"\"\n\n# Clamp BS_pred to reasonable range [8, 128]\nBS_PRED=$(python3 -c \"print(max(8, min(128, int('${BS_PRED}'))))\")\n\n# ── PHASE 2: DeepStream confirmation ────────────────────\necho \"PHASE 2: DeepStream confirmation around BS_pred=${BS_PRED}\"\n\n# Test BS_pred and BS_pred - small step if first fails\n# Round BS_pred to nearest sensible value\nBS_STEP=$(python3 -c \"\nbs = int('${BS_PRED}')\n# step = ~10% of BS_pred, minimum 1\nstep = max(1, round(bs * 0.1))\nprint(step)\n\")\n\nCANDIDATES=(\"${BS_PRED}\")\nBS_LOWER=$(( BS_PRED - BS_STEP ))\n[ \"${BS_LOWER}\" -ge 1 ] && CANDIDATES+=(\"${BS_LOWER}\")\n\nbest_bs=1\nbest_fps_stream=0\nbest_ips=0\nhad_valid_ds_run=false\n\nfor BS in \"${CANDIDATES[@]}\"; do\n echo \"\"\n echo \"=== DS Confirmation BS=${BS} (${BS} streams) ===\"\n\n # Write nvinfer config pointing to dynamic engine at this batch size\n BS_CONFIG=\"${CONFIGS_DIR}/config_infer_sweep_b${BS}.txt\"\n sed \\\n -e \"s|model-engine-file=.*|model-engine-file=${DYNAMIC_ENGINE}|\" \\\n -e \"s|batch-size=.*|batch-size=${BS}|\" \\\n -e \"s|custom-lib-path=.*|custom-lib-path=${PARSER_SO}|\" \\\n -e \"s|labelfile-path=.*|labelfile-path=${LABELS}|\" \\\n \"${CONFIG_TEMPLATE}\" > \"${BS_CONFIG}\"\n\n # actual frames = ACTUAL_FRAMES_PER_STREAM × BS (no num-buffers limit on filesrc —\n # let each source read to natural EOS so we always process the full video)\n TOTAL_FRAMES=$((ACTUAL_FRAMES_PER_STREAM * BS))\n # Build SOURCES as an array so a VIDEO path containing spaces or glob\n # characters survives shell expansion intact.\n SOURCES=()\n for ((i = 0; i \u003c BS; i++)); do\n SOURCES+=(\n filesrc \"location=${VIDEO}\" !\n qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue !\n \"mux.sink_${i}\"\n )\n done\n\n START_TIME=$(date +%s%N)\n GST_RC=0\n GST_DEBUG=0 gst-launch-1.0 -e \\\n \"${SOURCES[@]}\" \\\n nvstreammux name=mux batch-size=${BS} width=${MUXER_W} height=${MUXER_H} batched-push-timeout=40000 ! \\\n queue ! \\\n nvinfer config-file-path=\"${BS_CONFIG}\" ! \\\n queue ! \\\n fakesink sync=0 2>>\"${DS_ERR_LOG}\" || GST_RC=$?\n END_TIME=$(date +%s%N)\n ELAPSED_NS=$(( END_TIME - START_TIME ))\n\n # Reject runs that exited non-zero or finished implausibly fast — both\n # indicate a plugin/config error rather than a real benchmark result.\n # Scoring such a run would produce divide-by-zero or a bogus high FPS.\n if [ \"${GST_RC}\" -ne 0 ] || [ \"${ELAPSED_NS}\" -lt \"${NS_PER_SEC}\" ]; then\n echo \" [fail] gst-launch exit=${GST_RC} elapsed_ns=${ELAPSED_NS} — see ${DS_ERR_LOG}\" >&2\n continue\n fi\n had_valid_ds_run=true\n\n # Warn if the pipeline wrote anything to stderr — likely a plugin/config error\n if [ -s \"${DS_ERR_LOG}\" ]; then\n echo \" [warn] GStreamer stderr output captured — see ${DS_ERR_LOG} for details\" >&2\n fi\n\n # awk instead of bc (bc is not installed in the DeepStream container; awk always is)\n ELAPSED_SEC=$(awk \"BEGIN{printf \\\"%.2f\\\", ${ELAPSED_NS} / $NS_PER_SEC}\")\n DS_IPS=$(awk \"BEGIN{printf \\\"%.1f\\\", ${TOTAL_FRAMES} / ${ELAPSED_SEC}}\")\n DS_FPS_STREAM=$(awk \"BEGIN{printf \\\"%.1f\\\", ${DS_IPS} / ${BS}}\")\n DS_REALTIME=$(awk \"BEGIN{printf \\\"%.2f\\\", ${DS_FPS_STREAM} / ${FPS_THRESHOLD}}\")\n DS_FPS_INT=$(echo \"${DS_FPS_STREAM}\" | cut -d. -f1)\n DS_IPS_INT=$(echo \"${DS_IPS}\" | cut -d. -f1)\n\n echo \" BS=${BS}: wall=${ELAPSED_SEC}s imgs/s=${DS_IPS} fps/stream=${DS_FPS_STREAM} realtime=${DS_REALTIME}x\"\n\n if [ \"${DS_FPS_INT}\" -ge \"${FPS_THRESHOLD}\" ]; then\n best_bs=\"${BS}\"\n best_fps_stream=\"${DS_FPS_STREAM}\"\n best_ips=\"${DS_IPS_INT}\"\n echo \" -> PASS (>=${FPS_THRESHOLD} fps/stream)\"\n break # highest candidate that passes is BS_OPT\n else\n echo \" -> FAIL (\u003c${FPS_THRESHOLD} fps/stream), trying lower...\"\n fi\ndone\n\n# Abort if no candidate produced a valid DS run — emitting a default BS_OPT=1\n# in this case would mislead the caller into building a production engine on\n# top of a broken sweep.\nif [ \"${had_valid_ds_run}\" != true ]; then\n echo \"ERROR: no valid DeepStream confirmation run completed — see ${DS_ERR_LOG}\" >&2\n echo \" refusing to emit ${ENGINES_DIR}/bs_opt.txt\" >&2\n exit 1\nfi\n\n# Write results\necho \"\"\necho \"======================================================\"\necho \"SWEEP COMPLETE\"\necho \" BS_OPT = ${best_bs}\"\necho \" DS fps/stream = ${best_fps_stream} (threshold: ${FPS_THRESHOLD})\"\necho \" DS imgs/sec = ${best_ips}\"\necho \" trtexec alpha = ${ALPHA} (curve steepness)\"\necho \" BS_pred was = ${BS_PRED}\"\necho \"======================================================\"\n\ncat > \"${ENGINES_DIR}/bs_opt.txt\" \u003c\u003c EOF\nBS_OPT=${best_bs}\nDS_FPS_PER_STREAM=${best_fps_stream}\nDS_IPS=${best_ips}\nTRT_ALPHA=${ALPHA}\nTRT_A_COEFF=${A_COEFF}\nBS_PRED=${BS_PRED}\nEOF\n\n# Print probe summary\necho \"\"\necho \"Phase 1 trtexec probe summary:\"\necho \"batch,qps,imgs_per_sec\"\nfor i in \"${!PROBE_BS_ARR[@]}\"; do\n BS=\"${PROBE_BS_ARR[$i]}\"\n QPS=\"${PROBE_QPS_ARR[$i]}\"\n IPS=$(awk \"BEGIN{printf \\\"%d\\\", ${QPS} * ${BS}}\")\n echo \"${BS},${QPS},${IPS}\"\ndone\n\necho \"${best_bs}\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":12563,"content_sha256":"5f396d508a1387b0916e9020ef8c1fd150ed28b21230de59a8cbd3b6cffd1949"},{"filename":"scripts/deepstream/extract-frame.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -o pipefail\n################################################################################\n# Step 6: Extract first frame from output video as PNG for visual inspection.\n#\n# Usage: ./extract-frame.sh \u003cinput_video> \u003coutput_png>\n# Example: ./extract-frame.sh yolox_output.mp4 yolox_frame_sample.png\n################################################################################\n\nINPUT=\"$1\"\nOUTPUT=\"$2\"\n\nif [ -z \"$INPUT\" ] || [ -z \"$OUTPUT\" ]; then\n echo \"Usage: $0 \u003cinput_video> \u003coutput_png>\"\n exit 1\nfi\n\nif [[ \"$INPUT\" == *.ogv ]]; then\n gst-launch-1.0 \\\n filesrc location=\"${INPUT}\" ! oggdemux ! theoradec ! videoconvert ! \"video/x-raw,format=RGB\" ! \\\n pngenc snapshot=true ! filesink location=\"${OUTPUT}\" \\\n 2>&1 | grep -v \"^$\"\nelse\n gst-launch-1.0 \\\n filesrc location=\"${INPUT}\" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! \\\n nvvideoconvert ! \"video/x-raw,format=RGB\" ! videoconvert ! \\\n pngenc snapshot=true ! filesink location=\"${OUTPUT}\" \\\n 2>&1 | grep -v \"^$\"\nfi\nSTATUS=$?\n\nif [ $STATUS -eq 0 ] && [ -f \"$OUTPUT\" ]; then\n echo \"Frame extracted: ${OUTPUT} ($(ls -lh \"$OUTPUT\" | awk '{print $5}'))\"\nelse\n echo \"ERROR: Pipeline failed with exit code $STATUS\" >&2\n exit $STATUS\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1981,"content_sha256":"fe8d806f46ceae8108653f607e08db509115a37862a53ef9546e437d9bd451f4"},{"filename":"scripts/dsrun.sh","content":"#!/usr/bin/env bash\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\n# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Apache-2.0.\n#\n# Convenience wrapper: run a command INSIDE the DeepStream container with the working root\n# bind-mounted at /work. Every phase of this skill runs this way — nothing runs on the host.\n#\n# bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '\u003ccommand to run in-container>'\n# e.g. bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh 'build/.venv_optimum/bin/python -c \"import torch;print(torch.__version__)\"'\n# bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh 'make -C models/yolov8n/parser && ls models/yolov8n/parser/*.so'\n#\n# Env overrides: DS_IMAGE (default DeepStream 9.1), DS_GPU (default \"--gpus all\", set \"\" for CPU-only steps).\n# On PowerShell the -v token is \"${PWD}:/work\"; on cmd \"%cd%:/work\" — Claude Code sets it per host shell.\n# See references/windows.md.\nset -euo pipefail\nIMG=\"${DS_IMAGE:-nvcr.io/nvidia/deepstream:9.1-triton-multiarch}\"\nGPU=\"${DS_GPU-\"--gpus all\"}\"\nif [ \"$#\" -eq 0 ]; then echo \"usage: bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '\u003cin-container command>'\" >&2; exit 2; fi\nexec docker run --rm $GPU --shm-size=16g -v \"$PWD\":/work -w /work \\\n --entrypoint bash \"$IMG\" -lc \"$*\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1436,"content_sha256":"54a6938e0ca22f2551134351a9925c381bd0d2c74a5b2d044151c47bf4eed7f8"},{"filename":"scripts/engine/benchmark-trtexec.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n################################################################################\n# Step 8a: TensorRT benchmark using trtexec for arbitrary batch sizes.\n# Runs 10-second benchmarks and reports GPU compute time + throughput.\n#\n# Usage: ./benchmark-trtexec.sh \u003cbs:engine> [\u003cbs:engine> ...] [duration_sec]\n# Example: ./benchmark-trtexec.sh 1:yolox_nano_b1.engine 64:yolox_nano_b64.engine\n# ./benchmark-trtexec.sh 1:b1.engine 64:b64.engine 20\n################################################################################\n\n# Last plain-integer arg is treated as duration; all others are bs:engine pairs.\nDURATION=10\nENGINE_PAIRS=()\nfor arg in \"$@\"; do\n if [[ \"$arg\" =~ ^[0-9]+$ ]]; then\n DURATION=\"$arg\"\n else\n ENGINE_PAIRS+=(\"$arg\")\n fi\ndone\n\nif [ ${#ENGINE_PAIRS[@]} -eq 0 ]; then\n echo \"Usage: $0 \u003cbs:engine> [\u003cbs:engine> ...] [duration_sec]\"\n echo \" e.g. $0 1:model_b1.engine 64:model_b64.engine\"\n exit 1\nfi\n\nTRTEXEC=\"/usr/src/tensorrt/bin/trtexec\"\n\necho \"=== TensorRT Benchmark ===\"\necho \"Duration: ${DURATION}s per engine\"\necho \"\"\n\nfor ENGINE_INFO in \"${ENGINE_PAIRS[@]}\"; do\n BATCH=\"${ENGINE_INFO%%:*}\"\n ENGINE=\"${ENGINE_INFO#*:}\"\n\n if [ ! -f \"$ENGINE\" ]; then\n echo \"SKIP Batch ${BATCH}: ${ENGINE} not found\"\n echo \"\"\n continue\n fi\n\n echo \"--- Batch ${BATCH}: ${ENGINE} ---\"\n OUTPUT=$($TRTEXEC --loadEngine=\"$ENGINE\" --fp16 --duration=\"$DURATION\" 2>&1)\n\n THROUGHPUT=$(echo \"$OUTPUT\" | grep \"\\[I\\] Throughput:\" | grep -oP 'Throughput: \\K[0-9.]+')\n GPU_MEAN=$(echo \"$OUTPUT\" | grep \"GPU Compute Time:\" | grep -oP 'mean = \\K[0-9.]+')\n GPU_MIN=$(echo \"$OUTPUT\" | grep \"GPU Compute Time:\" | grep -oP 'min = \\K[0-9.]+')\n GPU_MAX=$(echo \"$OUTPUT\" | grep \"GPU Compute Time:\" | grep -oP 'max = \\K[0-9.]+')\n IMGS_PER_SEC=$(awk \"BEGIN{printf \\\"%d\\\", ${THROUGHPUT:-0} * $BATCH}\" 2>/dev/null) # awk: bc absent in container\n\n echo \" GPU Compute: ${GPU_MEAN} ms (min=${GPU_MIN}, max=${GPU_MAX})\"\n echo \" Throughput: ${THROUGHPUT} qps\"\n echo \" Images/sec: ${IMGS_PER_SEC}\"\n echo \"\"\ndone\n\necho \"=== Benchmark Complete ===\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2826,"content_sha256":"33b87c01352094988175339fa7c6fa8c60bade6b2e83eccbceb7c1d6e8e54935"},{"filename":"scripts/model/cleanup.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# cleanup.sh — Remove build/models artifacts for a given model name.\n# Validated replacement for ad-hoc directory removal after ONNX export.\n#\n# Only removes paths that:\n# - are non-empty\n# - exist\n# - resolve under ./build/ or ./models/\n# - match the given MODEL_NAME (regex-validated)\n#\n# Usage:\n# bash cleanup.sh \u003cMODEL_NAME> [--dry-run]\n#\n# Example:\n# bash cleanup.sh yolov8n\n# bash cleanup.sh yolov8n --dry-run\nset -euo pipefail\n\nMODEL_NAME=\"${1:-}\"\nDRY_RUN=false\nif [[ \"${2:-}\" == \"--dry-run\" ]]; then\n DRY_RUN=true\nfi\n\nif [[ -z \"$MODEL_NAME\" ]]; then\n echo \"Usage: $0 \u003cMODEL_NAME> [--dry-run]\" >&2\n exit 1\nfi\n\nif ! [[ \"$MODEL_NAME\" =~ ^[A-Za-z0-9._-]+$ ]]; then\n echo \"ERROR: MODEL_NAME must match ^[A-Za-z0-9._-]+$ (got: $MODEL_NAME)\" >&2\n exit 1\nfi\n\n# The regex above accepts \".\" and \"..\" — reject them explicitly since those\n# would make the candidate paths (build/.venv_$MODEL_NAME, models/$MODEL_NAME/*)\n# point at directories we don't own.\nif [[ \"$MODEL_NAME\" == \".\" || \"$MODEL_NAME\" == \"..\" ]]; then\n echo \"ERROR: MODEL_NAME cannot be '.' or '..' (got: $MODEL_NAME)\" >&2\n exit 1\nfi\n\nCWD=\"$(pwd -P)\"\n\n# Paths eligible for removal — all are scoped under CWD's build/ or models/\nCANDIDATES=(\n \"build/.venv_${MODEL_NAME}\"\n \"models/${MODEL_NAME}/hf_model\"\n \"models/${MODEL_NAME}/onnx_export\"\n)\n\necho \"=== cleanup.sh — MODEL_NAME=$MODEL_NAME dry-run=$DRY_RUN ===\"\nfor rel in \"${CANDIDATES[@]}\"; do\n abs=\"$CWD/$rel\"\n if [[ ! -e \"$abs\" ]]; then\n echo \" skip (not present): $rel\"\n continue\n fi\n\n # Assert the resolved path is still under CWD's build/ or models/\n resolved=\"$(cd \"$(dirname \"$abs\")\" && pwd -P)/$(basename \"$abs\")\"\n case \"$resolved\" in\n \"$CWD\"/build/*|\"$CWD\"/models/*) ;;\n *)\n echo \" SKIP (outside build/ or models/): $resolved\"\n continue\n ;;\n esac\n\n if $DRY_RUN; then\n echo \" [dry-run] rm -rf $resolved\"\n else\n echo \" removing: $resolved\"\n rm -rf -- \"$resolved\"\n fi\ndone\n\necho \"Done.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2774,"content_sha256":"4109f951adf7a03887cc1317e7c0c86d1c2d3083dc9ab97eb0f4f770a39ec0f4"},{"filename":"scripts/model/config-to-labels.py","content":"# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.\n# Licensed under the Apache License, Version 2.0 (the \"License\").\n\"\"\"Gate the architecture and write labels.txt, from a model's config.json.\n\nSingle implementation shared by both acquisition routes. The HuggingFace path (Step 2b)\nand the NGC path (Step 2d) reach config.json at different points but need identical\nbehaviour, and keeping two copies in the runbook meant the NGC route silently drifted —\nit extracted labels without ever rejecting non-detection architectures, so a\nclassification model could reach engine build. See references/model-acquire.md.\n\nUsage:\n python3 config-to-labels.py --config \u003cconfig.json> --labels \u003clabels.txt>\n python3 config-to-labels.py --config \u003cconfig.json> --labels \u003clabels.txt> --skip-arch-check\n\nExits non-zero on a non-detection architecture or a missing label map. Callers must treat\na non-zero exit as fatal: never fall back to COCO, ImageNet, or any other default list.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport sys\nfrom pathlib import Path\n\n# Detection heads this skill supports. DETR-family variants use the middle two.\nDETECTION_SUFFIXES = (\"ForObjectDetection\", \"ForConditionalDetection\", \"ForZeroShotObjectDetection\")\nNON_DETECTION_SUFFIXES = (\n \"ForImageClassification\", \"ForSemanticSegmentation\", \"ForInstanceSegmentation\",\n \"ForPanopticSegmentation\", \"ForDepthEstimation\", \"ForMaskedLM\",\n \"ForTokenClassification\", \"ForCausalLM\",\n)\n\n\ndef assert_detection(cfg: dict) -> None:\n \"\"\"Abort unless config.json declares an object-detection architecture.\"\"\"\n arch_list = cfg.get(\"architectures\") or []\n if not arch_list:\n print(\"[labels] config.json has no 'architectures' field; skipping architecture gate\",\n file=sys.stderr)\n return\n arch = arch_list[0]\n if arch.endswith(NON_DETECTION_SUFFIXES) or not arch.endswith(DETECTION_SUFFIXES):\n sys.exit(\n f\"ERROR: deepstream-import-vision-model currently supports object detection models \"\n f\"only. Detected architecture: {arch}. Classification, segmentation, and other vision \"\n f\"tasks are not yet supported.\"\n )\n print(f\"[labels] architecture OK: {arch}\")\n\n\ndef extract_labels(cfg: dict) -> list[str]:\n \"\"\"Pull the class list, trying each known config layout in order.\"\"\"\n if \"id2label\" in cfg: # standard HF layout\n return [cfg[\"id2label\"][str(i)] for i in range(len(cfg[\"id2label\"]))]\n if \"label2id\" in cfg: # reversed map\n return [k for k, _ in sorted(cfg[\"label2id\"].items(), key=lambda kv: kv[1])]\n if \"names\" in cfg: # some YOLO repos\n names = cfg[\"names\"]\n return [names[str(i)] for i in range(len(names))] if isinstance(names, dict) else list(names)\n sys.exit(\"ERROR: No label map found in config.json — cannot create labels.txt\")\n\n\ndef main() -> None:\n ap = argparse.ArgumentParser(description=\"Validate architecture and emit labels.txt.\")\n ap.add_argument(\"--config\", required=True, help=\"path to config.json\")\n ap.add_argument(\"--labels\", required=True, help=\"path to write labels.txt\")\n ap.add_argument(\"--skip-arch-check\", action=\"store_true\",\n help=\"emit labels without gating the architecture (diagnostics only)\")\n args = ap.parse_args()\n\n try:\n cfg = json.loads(Path(args.config).read_text())\n except (OSError, json.JSONDecodeError) as exc:\n sys.exit(f\"ERROR: could not read {args.config}: {exc}\")\n\n if not args.skip_arch_check:\n assert_detection(cfg)\n\n labels = extract_labels(cfg)\n out = Path(args.labels)\n out.parent.mkdir(parents=True, exist_ok=True)\n out.write_text(\"\\n\".join(labels) + \"\\n\")\n print(f\"labels.txt: {len(labels)} classes -> {out}\")\n print(\" \" + \", \".join(labels[:5]) + (\" ...\" if len(labels) > 5 else \"\"))\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4154,"content_sha256":"49c05661c294044b383e44ee30ccdefbf899dc0c7d30f55e261d472965b209fe"},{"filename":"scripts/model/hf-download-config.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# hf-download-config.sh — Download config.json from a HuggingFace repo.\n# Safer replacement for the inline `curl -fsSL ... -o ...` snippet.\n#\n# Usage:\n# bash hf-download-config.sh \u003cHF_ORG> \u003cMODEL_NAME> \u003cDEST_PATH>\n#\n# Example:\n# bash hf-download-config.sh onnx-community yolov8n models/yolov8n/config/config.json\n#\n# Honors $HF_TOKEN if set.\nset -euo pipefail\n\nHF_ORG=\"${1:-}\"\nMODEL_NAME=\"${2:-}\"\nDEST=\"${3:-}\"\n\nif [[ -z \"$HF_ORG\" || -z \"$MODEL_NAME\" || -z \"$DEST\" ]]; then\n echo \"Usage: $0 \u003cHF_ORG> \u003cMODEL_NAME> \u003cDEST_PATH>\" >&2\n exit 1\nfi\n\nfor arg_name in HF_ORG MODEL_NAME; do\n val=\"${!arg_name}\"\n if ! [[ \"$val\" =~ ^[A-Za-z0-9._/-]+$ ]]; then\n echo \"ERROR: $arg_name contains invalid characters: $val\" >&2\n exit 1\n fi\ndone\n\n# DEST must be a relative path and must not contain .. segments\n# (prevents writes outside the project tree)\ncase \"$DEST\" in\n /*)\n echo \"ERROR: DEST_PATH must be relative (absolute paths are rejected): $DEST\" >&2\n exit 1\n ;;\n *..*)\n echo \"ERROR: DEST_PATH contains '..' — refusing: $DEST\" >&2\n exit 1\n ;;\nesac\n\nURL=\"https://huggingface.co/${HF_ORG}/${MODEL_NAME}/resolve/main/config.json\"\n\nCURL_OPTS=(-fsSL --proto '=https' --tlsv1.2 --max-time 60 -o \"$DEST\")\nif [[ -n \"${HF_TOKEN:-}\" ]]; then\n CURL_OPTS+=(-H \"Authorization: Bearer ${HF_TOKEN}\")\nfi\n\nmkdir -p \"$(dirname \"$DEST\")\"\n\nif ! curl \"${CURL_OPTS[@]}\" \"$URL\"; then\n echo \"ERROR: config.json not found at ${HF_ORG}/${MODEL_NAME} — cannot extract labels\" >&2\n exit 1\nfi\n\necho \"Downloaded: $DEST\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2277,"content_sha256":"b0d6000e29f75e995d97e82ee294f887f6cae48c527f6c652f286003a03f3811"},{"filename":"scripts/model/hf-list-files.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# hf-list-files.sh — List model files in a HuggingFace repo.\n# Uses the HF tree API with validated inputs, HTTPS+TLSv1.2, and a bounded\n# timeout. Parses the JSON response via the stdlib json module (no shell pipe).\n#\n# Usage:\n# bash hf-list-files.sh \u003cHF_ORG> \u003cMODEL_NAME> [subpath]\n#\n# Examples:\n# bash hf-list-files.sh onnx-community yolov8n\n# bash hf-list-files.sh onnx-community yolov8n onnx # check /onnx subdir\n#\n# Honors $HF_TOKEN if set (passed as Authorization: Bearer header).\nset -euo pipefail\n\nHF_ORG=\"${1:-}\"\nMODEL_NAME=\"${2:-}\"\nSUBPATH=\"${3:-}\"\n\nif [[ -z \"$HF_ORG\" || -z \"$MODEL_NAME\" ]]; then\n echo \"Usage: $0 \u003cHF_ORG> \u003cMODEL_NAME> [subpath]\" >&2\n exit 1\nfi\n\n# Input validation — reject anything that could escape the URL\nfor arg_name in HF_ORG MODEL_NAME SUBPATH; do\n val=\"${!arg_name:-}\"\n if [[ -n \"$val\" ]] && ! [[ \"$val\" =~ ^[A-Za-z0-9._/-]+$ ]]; then\n echo \"ERROR: $arg_name contains invalid characters (must match ^[A-Za-z0-9._/-]+\\$): $val\" >&2\n exit 1\n fi\ndone\n\nURL=\"https://huggingface.co/api/models/${HF_ORG}/${MODEL_NAME}/tree/main\"\n[[ -n \"$SUBPATH\" ]] && URL=\"${URL}/${SUBPATH}\"\n\n# -sS: silent progress but still surface errors on stderr\n# -w \"%{http_code}\": append HTTP status as the last 3 chars of the response body\n# Drop -f so curl doesn't exit non-zero on 4xx — we inspect the status ourselves\n# so 404 (missing subpath) can be distinguished from network/auth failures.\nCURL_OPTS=(-sS --proto '=https' --tlsv1.2 --max-time 30 -w '%{http_code}')\nif [[ -n \"${HF_TOKEN:-}\" ]]; then\n CURL_OPTS+=(-H \"Authorization: Bearer ${HF_TOKEN}\")\nfi\n\n# Separate exit-code capture from body so we can diagnose failures precisely.\nCURL_RC=0\nRESPONSE=\"$(curl \"${CURL_OPTS[@]}\" \"$URL\")\" || CURL_RC=$?\n\nif [[ $CURL_RC -ne 0 ]]; then\n echo \"ERROR: curl failed (exit $CURL_RC) while fetching $URL\" >&2\n exit 1\nfi\n\n# -w appends the 3-digit status to the body; split them back apart.\nHTTP_CODE=\"${RESPONSE: -3}\"\nJSON=\"${RESPONSE:0:${#RESPONSE}-3}\"\n\ncase \"$HTTP_CODE\" in\n 200) ;; # fall through to parsing\n 404)\n # Acceptable: the requested subpath (e.g. /onnx) doesn't exist.\n exit 0\n ;;\n 401|403)\n echo \"ERROR: HTTP $HTTP_CODE from HuggingFace for $URL (auth/permission)\" >&2\n exit 1\n ;;\n *)\n echo \"ERROR: HTTP $HTTP_CODE from HuggingFace for $URL\" >&2\n exit 1\n ;;\nesac\n\n# 200 but empty body is unexpected — surface it rather than silently swallow.\nif [[ -z \"$JSON\" ]]; then\n echo \"ERROR: HTTP 200 but empty body from $URL\" >&2\n exit 1\nfi\n\n# Parse via python3 (json module is stdlib). Each line: \u003cpath>\npython3 - \"$JSON\" \u003c\u003c'PYEOF'\nimport json, sys\ndata = sys.argv[1]\ntry:\n entries = json.loads(data)\nexcept json.JSONDecodeError as e:\n # Surface the decode error so callers can distinguish \"empty repo\" from\n # \"HF returned something we can't parse\" (upstream format change, captive\n # portal HTML, etc.). Truncate the raw data so we don't dump a multi-MB\n # response into logs.\n preview = data[:500] + (\"... [truncated]\" if len(data) > 500 else \"\")\n print(f\"ERROR: failed to parse JSON from HuggingFace API: {e}\", file=sys.stderr)\n print(f\" raw response: {preview!r}\", file=sys.stderr)\n sys.exit(1)\nif not isinstance(entries, list):\n preview = repr(entries)[:500]\n print(\n f\"ERROR: unexpected response type from HuggingFace API: \"\n f\"{type(entries).__name__} (expected list)\",\n file=sys.stderr,\n )\n print(f\" contents: {preview}\", file=sys.stderr)\n sys.exit(1)\n# Empty list is valid (directory exists but has no files) — exit 0 silently.\nfor e in entries:\n p = e.get(\"path\") if isinstance(e, dict) else None\n if p:\n print(p)\nPYEOF\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4462,"content_sha256":"b6e0ff48041ecf0e6a7be3b295fae597353ae020227ebfa3bebe951024d55cb6"},{"filename":"scripts/model/inspect-onnx.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nStep 1: Inspect an ONNX model — inputs, outputs, opset, operators, validity.\nUsage: python3 inspect-onnx.py \u003connx_file>\n\"\"\"\nimport sys\nimport onnx\n\nif len(sys.argv) != 2:\n print(f\"Usage: {sys.argv[0]} \u003connx_file>\")\n sys.exit(1)\n\ntry:\n model = onnx.load(sys.argv[1])\nexcept FileNotFoundError:\n print(f\"Error: File '{sys.argv[1]}' not found\")\n sys.exit(1)\nexcept Exception as e:\n print(f\"Error loading ONNX model: {e}\")\n sys.exit(1)\n\nprint(\"=== ONNX Model Info ===\")\nprint(f\"File: {sys.argv[1]}\")\nopset_ver = model.opset_import[0].version if model.opset_import else \"N/A\"\nprint(f\"Opset: {opset_ver}\")\nprint(f\"IR ver: {model.ir_version}\")\nprint(f\"Producer: {model.producer_name} {model.producer_version}\")\n\ngraph = getattr(model, \"graph\", None)\nif graph is None:\n print(\"Error: ONNX model has no graph\")\n sys.exit(1)\n\nprint(f\"Nodes: {len(graph.node)}\")\n\ndtype_map = {1: \"float32\", 10: \"float16\", 7: \"int64\", 6: \"int32\", 9: \"bool\"}\n\nprint(\"\\n=== INPUTS ===\")\nfor inp in graph.input:\n shape = [d.dim_value if d.dim_value else d.dim_param for d in inp.type.tensor_type.shape.dim]\n dtype = dtype_map.get(inp.type.tensor_type.elem_type, inp.type.tensor_type.elem_type)\n print(f\" {inp.name}: shape={shape}, dtype={dtype}\")\n\nprint(\"\\n=== OUTPUTS ===\")\nfor out in graph.output:\n shape = [d.dim_value if d.dim_value else d.dim_param for d in out.type.tensor_type.shape.dim]\n dtype = dtype_map.get(out.type.tensor_type.elem_type, out.type.tensor_type.elem_type)\n print(f\" {out.name}: shape={shape}, dtype={dtype}\")\n\nprint(\"\\n=== Operators ===\")\nop_types = sorted(set(n.op_type for n in graph.node))\nprint(f\" {', '.join(op_types)}\")\nprint(f\" Total unique ops: {len(op_types)}\")\n\ntry:\n onnx.checker.check_model(model)\n print(\"\\n✓ ONNX model is valid\")\nexcept Exception as e:\n print(f\"\\n✗ ONNX validation error: {e}\")\n\n# --- Machine-parseable summary (consumed by the engine-build and pipeline-run phases) ---\n# grep patterns expect lines: \"input_name: \u003cname>\", \"height: \u003cint>\", \"width: \u003cint>\"\nprint(\"\\n=== Machine-Parseable Summary ===\")\nif graph and graph.input:\n inp = graph.input[0]\n dims = inp.type.tensor_type.shape.dim\n print(f\"input_name: {inp.name}\")\n if len(dims) >= 4:\n # Assume NCHW: dim[0]=batch, dim[1]=channels, dim[2]=H, dim[3]=W\n h_val = dims[2].dim_value # 0 means dynamic\n w_val = dims[3].dim_value\n if h_val > 0 and w_val > 0:\n print(f\"height: {h_val}\")\n print(f\"width: {w_val}\")\n else:\n # Dynamic spatial dims — print symbol so callers can detect failure\n print(f\"height: DYNAMIC (symbol={dims[2].dim_param or 'unknown'})\")\n print(f\"width: DYNAMIC (symbol={dims[3].dim_param or 'unknown'})\")\n print(\"WARNING: Dynamic H/W — set height and width manually in trtexec flags\")\n else:\n print(f\"WARNING: Input has {len(dims)} dims — expected 4 (NCHW); cannot auto-detect H/W\")\nelse:\n print(\"WARNING: No inputs found in ONNX graph\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3747,"content_sha256":"1147e71d6d069aa7c982c23f9c3de225f7a9aa5efebe8b033614f44f10e035d2"},{"filename":"scripts/model/make-static-batch-onnx.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nStep 7: Create static-batch ONNX files from a batch-1 ONNX.\nPatches input/output batch dims and internal Reshape nodes.\n\nUsage: python3 make-static-batch-onnx.py \u003csrc_onnx> \u003cdst_onnx> \u003cbatch_size>\nExample: python3 make-static-batch-onnx.py yolox_nano.onnx b16/yolox_nano_b16.onnx 16\n\"\"\"\nimport sys\nimport onnx\nimport numpy as np\nfrom onnx import numpy_helper\n\nif len(sys.argv) != 4:\n print(f\"Usage: {sys.argv[0]} \u003csrc_onnx> \u003cdst_onnx> \u003cbatch_size>\")\n sys.exit(1)\n\nsrc_path = sys.argv[1]\ndst_path = sys.argv[2]\ntry:\n batch_size = int(sys.argv[3])\n if batch_size \u003c= 0:\n raise ValueError(\"Batch size must be positive\")\nexcept ValueError as e:\n print(f\"Error: Invalid batch size '{sys.argv[3]}': {e}\")\n sys.exit(1)\n\ntry:\n model = onnx.load(src_path)\nexcept FileNotFoundError:\n print(f\"Error: File '{src_path}' not found\")\n sys.exit(1)\nexcept Exception as e:\n print(f\"Error loading ONNX model: {e}\")\n sys.exit(1)\n\ngraph = getattr(model, \"graph\", None)\nif graph is None:\n print(\"Error: ONNX model has no graph\")\n sys.exit(1)\n\n# Set static batch on inputs\nfor inp in graph.input:\n if len(inp.type.tensor_type.shape.dim) > 0:\n inp.type.tensor_type.shape.dim[0].dim_param = \"\"\n inp.type.tensor_type.shape.dim[0].dim_value = batch_size\n\n# Set static batch on outputs\nfor out in graph.output:\n if len(out.type.tensor_type.shape.dim) > 0:\n out.type.tensor_type.shape.dim[0].dim_param = \"\"\n out.type.tensor_type.shape.dim[0].dim_value = batch_size\n\n# Fix Reshape nodes that reference batch=1\nfor node in graph.node:\n if node.op_type == \"Reshape\":\n shape_input = node.input[1]\n for init in graph.initializer:\n if init.name == shape_input:\n shape_data = numpy_helper.to_array(init).copy()\n if shape_data.size > 0 and shape_data[0] == 1:\n shape_data[0] = batch_size\n init.CopyFrom(numpy_helper.from_array(shape_data, name=init.name))\n\nonnx.save(model, dst_path)\nprint(f\"Saved {dst_path} with batch={batch_size}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2781,"content_sha256":"1e3b622aa005d7318aec72eb6c0e965917e17cd4ea30b92bb071394fa1bd67dc"},{"filename":"scripts/model/ngc-download.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# ngc-download.sh — Download all files from a public NGC model version.\n# Prefers the official `ngc` CLI. Falls back to authenticated HTTPS via curl\n# only when the CLI is not installed; the fallback is explicitly warned about.\n#\n# Usage:\n# bash ngc-download.sh \u003cNGC_ORG> \u003cNGC_TEAM> \u003cMODEL_NAME> \u003cNGC_VERSION> \u003cDEST_DIR>\n#\n# Example:\n# bash ngc-download.sh nvidia tao peoplenet trainable_v2.6 models/peoplenet/ngc_download\nset -euo pipefail\n\nNGC_ORG=\"${1:-}\"\nNGC_TEAM=\"${2:-}\"\nMODEL_NAME=\"${3:-}\"\nNGC_VERSION=\"${4:-}\"\nDEST_DIR=\"${5:-}\"\n\nif [[ -z \"$NGC_ORG\" || -z \"$MODEL_NAME\" || -z \"$NGC_VERSION\" || -z \"$DEST_DIR\" ]]; then\n echo \"Usage: $0 \u003cNGC_ORG> \u003cNGC_TEAM> \u003cMODEL_NAME> \u003cNGC_VERSION> \u003cDEST_DIR>\" >&2\n echo \" NGC_TEAM may be empty-string if the model has no team segment.\" >&2\n exit 1\nfi\n\nfor var in NGC_ORG MODEL_NAME NGC_VERSION; do\n val=\"${!var}\"\n if ! [[ \"$val\" =~ ^[A-Za-z0-9._-]+$ ]]; then\n echo \"ERROR: $var contains invalid characters: $val\" >&2\n exit 1\n fi\ndone\nif [[ -n \"$NGC_TEAM\" ]] && ! [[ \"$NGC_TEAM\" =~ ^[A-Za-z0-9._-]+$ ]]; then\n echo \"ERROR: NGC_TEAM contains invalid characters: $NGC_TEAM\" >&2\n exit 1\nfi\n\ncase \"$DEST_DIR\" in\n \"\"|\"/\"|*..*)\n echo \"ERROR: invalid DEST_DIR: $DEST_DIR\" >&2\n exit 1\n ;;\nesac\n\nmkdir -p \"$DEST_DIR\"\n\n# Preferred: ngc CLI (authenticated, verified)\nif command -v ngc >/dev/null 2>&1 && ngc --version >/dev/null 2>&1; then\n if [[ -n \"$NGC_TEAM\" ]]; then\n SPEC=\"${NGC_ORG}/${NGC_TEAM}/${MODEL_NAME}:${NGC_VERSION}\"\n else\n SPEC=\"${NGC_ORG}/${MODEL_NAME}:${NGC_VERSION}\"\n fi\n echo \"Using ngc CLI to download $SPEC -> $DEST_DIR\"\n ngc registry model download-version \"$SPEC\" --dest \"$DEST_DIR\"\n exit 0\nfi\n\n# Fallback: HTTPS via curl, public NGC catalog API only\necho \"WARNING: ngc CLI not available — falling back to unauthenticated HTTPS for public files.\" >&2\necho \" For gated/private models, install the ngc CLI: https://ngc.nvidia.com/setup/installers/cli\" >&2\n\nif [[ -n \"$NGC_TEAM\" ]]; then\n NGC_BASE=\"https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${NGC_TEAM}/${MODEL_NAME}/versions/${NGC_VERSION}/files\"\nelse\n NGC_BASE=\"https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${MODEL_NAME}/versions/${NGC_VERSION}/files\"\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nFILES=\"$(\"$SCRIPT_DIR/ngc-list-files.sh\" \"$NGC_ORG\" \"$NGC_TEAM\" \"$MODEL_NAME\" \"$NGC_VERSION\")\"\n\nif [[ -z \"$FILES\" ]]; then\n echo \"ERROR: No files returned from NGC catalog\" >&2\n exit 1\nfi\n\necho \"NGC files available:\"\necho \"$FILES\"\n\nwhile IFS= read -r FNAME; do\n [[ -z \"$FNAME\" ]] && continue\n # Skip anything with traversal characters\n case \"$FNAME\" in\n */..*|..*|*..|/*)\n echo \" skipping suspicious filename: $FNAME\"\n continue\n ;;\n esac\n DEST_PATH=\"$DEST_DIR/$FNAME\"\n mkdir -p \"$(dirname \"$DEST_PATH\")\"\n echo \"Downloading: $FNAME\"\n if ! curl -fsSL --proto '=https' --tlsv1.2 --max-time 600 \\\n -o \"$DEST_PATH\" \"${NGC_BASE}/${FNAME}\"; then\n echo \" WARNING: failed to download $FNAME — skipping\"\n fi\ndone \u003c\u003c\u003c \"$FILES\"\n\necho \"Done. Files in $DEST_DIR:\"\nls -lh \"$DEST_DIR\" 2>/dev/null || true\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3907,"content_sha256":"44b2ccaa8cddb70d521d770621a3aa1d5ee542cf6f5130f858027a2997163d3b"},{"filename":"scripts/model/ngc-list-files.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# ngc-list-files.sh — List files in a public NGC model version.\n# Safer replacement for the inline curl+python snippet.\n#\n# Usage:\n# bash ngc-list-files.sh \u003cNGC_ORG> \u003cNGC_TEAM> \u003cMODEL_NAME> \u003cNGC_VERSION>\n#\n# Example:\n# bash ngc-list-files.sh nvidia tao peoplenet trainable_v2.6\n#\n# Output: one filename per line.\nset -euo pipefail\n\nNGC_ORG=\"${1:-}\"\nNGC_TEAM=\"${2:-}\"\nMODEL_NAME=\"${3:-}\"\nNGC_VERSION=\"${4:-}\"\n\nif [[ -z \"$NGC_ORG\" || -z \"$MODEL_NAME\" || -z \"$NGC_VERSION\" ]]; then\n echo \"Usage: $0 \u003cNGC_ORG> \u003cNGC_TEAM> \u003cMODEL_NAME> \u003cNGC_VERSION>\" >&2\n echo \" NGC_TEAM may be empty-string if the model has no team segment.\" >&2\n exit 1\nfi\n\nfor var in NGC_ORG MODEL_NAME NGC_VERSION; do\n val=\"${!var}\"\n if ! [[ \"$val\" =~ ^[A-Za-z0-9._-]+$ ]]; then\n echo \"ERROR: $var contains invalid characters: $val\" >&2\n exit 1\n fi\ndone\nif [[ -n \"$NGC_TEAM\" ]] && ! [[ \"$NGC_TEAM\" =~ ^[A-Za-z0-9._-]+$ ]]; then\n echo \"ERROR: NGC_TEAM contains invalid characters: $NGC_TEAM\" >&2\n exit 1\nfi\n\nif [[ -n \"$NGC_TEAM\" ]]; then\n NGC_BASE=\"https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${NGC_TEAM}/${MODEL_NAME}/versions/${NGC_VERSION}/files\"\nelse\n NGC_BASE=\"https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${MODEL_NAME}/versions/${NGC_VERSION}/files\"\nfi\n\nif ! JSON=\"$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 \"${NGC_BASE}/\" 2>/dev/null)\"; then\n echo \"ERROR: Could not retrieve file list from NGC API\" >&2\n echo \"URL: ${NGC_BASE}/\" >&2\n exit 1\nfi\n\npython3 - \"$JSON\" \"${NGC_BASE}/\" \u003c\u003c'PYEOF'\nimport json, sys\ndata, url = sys.argv[1], sys.argv[2]\ntry:\n files = json.loads(data)\nexcept json.JSONDecodeError as e:\n print(f\"ERROR parsing NGC file list: {e}\", file=sys.stderr)\n sys.exit(1)\nif isinstance(files, list):\n names = [f.get(\"name\", \"\") for f in files if isinstance(f, dict)]\nelse:\n names = [f.get(\"name\", \"\") for f in files.get(\"modelFiles\", []) if isinstance(f, dict)]\nnames = [n for n in names if n]\nif not names:\n print(f\"ERROR: NGC API returned no model files at {url}\", file=sys.stderr)\n sys.exit(1)\nfor n in names:\n print(n)\nPYEOF\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2812,"content_sha256":"316c1c9740baba57bc24653d812165f922d18d7987a37ad1327fb6821b1694ad"},{"filename":"scripts/model/resolve-engine.sh","content":"#!/usr/bin/env bash\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\n# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Apache-2.0.\n#\n# Resolve the engine that batch scaling actually produced, plus the values derived from it.\n#\n# WHY a helper: Step 6/7 (pipeline-run.md) and Step 8 (report-generation.md) both need the same\n# engine, and both used to inline the same lookup. They drifted — pipeline-run.md selected with\n# `head -1` and so picked b1 while report-generation.md picked the highest MAX_BS, meaning the\n# benchmark and the report could describe different engines. One implementation cannot drift.\n#\n# Selection rule: `sort -V | tail -1` takes the HIGHEST MAX_BS, i.e. the final engine from\n# iterative scaling. A lexicographic `head -1` would take b1 out of b1/b16/b8.\n#\n# USAGE — evaluate the output to set ENGINE / MAX_BS / MODEL_FILENAME in the caller's shell:\n# eval \"$(bash .claude/skills/deepstream-import-vision-model/scripts/model/resolve-engine.sh \"$MODEL_NAME\")\"\n#\n# Exits non-zero with a message on stderr when no engine is present.\nset -euo pipefail\n\nMODEL_NAME=\"${1:?usage: resolve-engine.sh \u003cMODEL_NAME>}\"\nENGINE_DIR=\"models/${MODEL_NAME}/benchmarks/engines\"\n\nENGINE=$(ls \"${ENGINE_DIR}\"/*_dynamic_b*.engine 2>/dev/null | sort -V | tail -1 || true)\nif [ -z \"$ENGINE\" ]; then\n echo \"ERROR: No engine found in ${ENGINE_DIR}/ — run Steps 4-5 first (references/engine-build.md)\" >&2\n exit 1\nfi\n\nMAX_BS=$(echo \"$ENGINE\" | grep -oP '_b\\K[0-9]+(?=\\.engine)')\nMODEL_FILENAME=$(basename \"$ENGINE\" | sed 's/_dynamic_b[0-9]*\\.engine//')\n\nprintf 'ENGINE=%q\\n' \"$ENGINE\"\nprintf 'MAX_BS=%q\\n' \"$MAX_BS\"\nprintf 'MODEL_FILENAME=%q\\n' \"$MODEL_FILENAME\"\nprintf 'echo \"Using engine: %s (MAX_BS=%s)\"\\n' \"$ENGINE\" \"$MAX_BS\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1865,"content_sha256":"bdc2222ddae0efbd598f5af55d75a30893861a768cb051c5fc13d68100bb0217"},{"filename":"scripts/model/safetensors_to_onnx.py","content":"# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.\n# Licensed under the Apache License, Version 2.0 (the \"License\").\n\"\"\"SafeTensors -> ONNX export for object-detection models, via torch.onnx.export.\n\nReplaces the former `optimum-cli export onnx` path. optimum pinned transformers below\n4.54.0, which made it impossible to reach the transformers releases that fix the RCE\nadvisories (5.3.0 / 5.5.0); optimum 2.1.0 also dropped the `onnx` subcommand entirely.\n\nScope is object detection, matching the skill. Every supported DETR-family detector\n(DETR, Conditional DETR, Deformable DETR, RT-DETR, YOLOS) exposes the same contract:\none `pixel_values` input and `logits` + `pred_boxes` outputs. That uniformity is what\nmakes a single generic exporter sufficient here.\n\nWrites `\u003coutput_dir>/model.onnx` — the same filename optimum produced, so downstream\nsteps are unchanged.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport sys\nfrom pathlib import Path\n\nimport torch\nfrom transformers import AutoConfig, AutoModelForObjectDetection\n\nDEFAULT_SIZE = 640\nDETECTION_SUFFIXES = (\"ForObjectDetection\", \"ForConditionalDetection\", \"ForZeroShotObjectDetection\")\n\n\ndef resolve_input_size(model_dir: str, override: int | None) -> tuple[int, int]:\n \"\"\"Pick the export resolution: explicit override, else preprocessor_config.json, else 640.\"\"\"\n if override:\n return override, override\n\n cfg_path = Path(model_dir) / \"preprocessor_config.json\"\n if cfg_path.is_file():\n try:\n size = json.loads(cfg_path.read_text()).get(\"size\") or {}\n except (json.JSONDecodeError, OSError):\n size = {}\n if isinstance(size, dict):\n if \"height\" in size and \"width\" in size:\n return int(size[\"height\"]), int(size[\"width\"])\n # DETR-style resize: a single shortest/longest edge. Export square at the\n # shortest edge — nvinfer feeds fixed-size letterboxed frames anyway.\n edge = size.get(\"shortest_edge\") or size.get(\"longest_edge\")\n if edge:\n return int(edge), int(edge)\n elif isinstance(size, int):\n return size, size\n\n return DEFAULT_SIZE, DEFAULT_SIZE\n\n\ndef assert_detection_architecture(model_id: str, revision: str) -> None:\n \"\"\"Fail fast on non-detection models, matching the skill's stated scope.\"\"\"\n try:\n config = AutoConfig.from_pretrained(model_id, revision=revision)\n except Exception as exc: # noqa: BLE001 - surface the loader's own message\n sys.exit(f\"ERROR: could not read model config for {model_id!r}: {exc}\")\n\n arch_list = getattr(config, \"architectures\", None) or []\n if arch_list and not any(a.endswith(DETECTION_SUFFIXES) for a in arch_list):\n sys.exit(\n f\"ERROR: deepstream-import-vision-model supports object detection models only. \"\n f\"Detected architecture: {arch_list[0]}. Classification, segmentation, and other \"\n f\"vision tasks are not supported.\"\n )\n\n\nclass DetectionWrapper(torch.nn.Module):\n \"\"\"Reduce the HF output object to the two tensors DeepStream's parser consumes.\"\"\"\n\n def __init__(self, model: torch.nn.Module) -> None:\n super().__init__()\n self.model = model\n\n def forward(self, pixel_values: torch.Tensor):\n out = self.model(pixel_values=pixel_values)\n return out.logits, out.pred_boxes\n\n\ndef consolidate_external_data(onnx_path: Path) -> None:\n \"\"\"Fold any sidecar `.onnx.data` back into the model file.\n\n torch.onnx.export splits tensors out for large models; trtexec expects one file.\n \"\"\"\n sidecar = onnx_path.with_suffix(onnx_path.suffix + \".data\")\n if not sidecar.exists():\n return\n import onnx\n\n model = onnx.load(str(onnx_path), load_external_data=True)\n onnx.save(model, str(onnx_path))\n sidecar.unlink()\n print(f\"[export] consolidated external data ({sidecar.name}) into {onnx_path.name}\")\n\n\ndef verify(onnx_path: Path) -> bool:\n \"\"\"Assert the DeepStream input/output contract; return whether batch stayed dynamic.\n\n Neither backend reliably honours a dynamic batch dimension: the dynamo exporter can\n specialize it where the model's own code captures `shape[0]`, so it is checked rather\n than assumed.\n \"\"\"\n import onnx\n\n model = onnx.load(str(onnx_path))\n onnx.checker.check_model(model)\n\n inputs = {i.name: i for i in model.graph.input}\n outputs = [o.name for o in model.graph.output]\n print(f\"[export] inputs={list(inputs)} outputs={outputs}\")\n\n if \"pixel_values\" not in inputs:\n sys.exit(f\"ERROR: exported graph has no 'pixel_values' input (got {list(inputs)})\")\n for required in (\"logits\", \"pred_boxes\"):\n if required not in outputs:\n sys.exit(f\"ERROR: exported graph is missing the '{required}' output (got {outputs})\")\n\n dims = inputs[\"pixel_values\"].type.tensor_type.shape.dim\n shape = [d.dim_param or d.dim_value for d in dims]\n print(f\"[export] pixel_values shape={shape}\")\n return bool(dims[0].dim_param)\n\n\ndef export_dynamo(wrapper, h, w, onnx_path, opset, static_batch, max_batch) -> None:\n \"\"\"torch.export-based exporter. Handles models the TorchScript tracer cannot.\"\"\"\n # Trace with batch=2 so torch.export cannot specialize the dimension to the constant 1.\n dummy = torch.randn(1 if static_batch else 2, 3, h, w)\n shapes = None if static_batch else {\n \"pixel_values\": {0: torch.export.Dim(\"batch\", min=1, max=max_batch)},\n }\n torch.onnx.export(\n wrapper, (dummy,), str(onnx_path), dynamic_shapes=shapes, dynamo=True,\n input_names=[\"pixel_values\"], output_names=[\"logits\", \"pred_boxes\"],\n opset_version=opset,\n )\n\n\ndef export_torchscript(wrapper, h, w, onnx_path, opset, static_batch, max_batch) -> None:\n \"\"\"Legacy TorchScript exporter.\n\n Still the more reliable path for a dynamic batch dimension on DETR-family detectors —\n RT-DETR specializes batch under dynamo but stays dynamic here. It does fail on\n text-conditioned models (Grounding DINO and friends), which is what dynamo is for.\n \"\"\"\n axes = None if static_batch else {\n \"pixel_values\": {0: \"batch\"}, \"logits\": {0: \"batch\"}, \"pred_boxes\": {0: \"batch\"},\n }\n torch.onnx.export(\n wrapper, torch.randn(1, 3, h, w), str(onnx_path), dynamic_axes=axes,\n do_constant_folding=True, dynamo=False,\n input_names=[\"pixel_values\"], output_names=[\"logits\", \"pred_boxes\"],\n opset_version=opset,\n )\n\n\ndef main() -> None:\n ap = argparse.ArgumentParser(description=\"Export a HuggingFace detection model to ONNX.\")\n ap.add_argument(\"--model\", required=True, help=\"HF model id or local model directory\")\n ap.add_argument(\"--output-dir\", required=True, help=\"directory to write model.onnx into\")\n # 18, not 17: the dynamo exporter implements >=18 and auto-upgrades anyway, and its\n # downgrade path fails outright on Resize (\"No Adapter To Version 17 for Resize\").\n # Opset 18 is compatible with TRT 10.16 — see references/engine-build.md.\n ap.add_argument(\"--opset\", type=int, default=18, help=\"ONNX opset (default 18)\")\n ap.add_argument(\"--image-size\", type=int, default=None,\n help=\"square export size; default reads preprocessor_config.json, else 640\")\n ap.add_argument(\"--max-batch\", type=int, default=16,\n help=\"upper bound for the dynamic batch dimension (default 16)\")\n ap.add_argument(\"--static-batch\", action=\"store_true\",\n help=\"export a fixed batch-1 graph instead of a dynamic batch dimension\")\n ap.add_argument(\"--revision\", default=\"main\",\n help=\"Hub revision (branch, tag, or commit SHA). Pin a SHA for reproducible builds.\")\n ap.add_argument(\"--legacy-torchscript\", action=\"store_true\",\n help=\"force the TorchScript exporter instead of trying dynamo first\")\n args = ap.parse_args()\n\n assert_detection_architecture(args.model, args.revision)\n\n h, w = resolve_input_size(args.model, args.image_size)\n print(f\"[export] model={args.model} input=1x3x{h}x{w} opset={args.opset}\")\n\n model = AutoModelForObjectDetection.from_pretrained(args.model, revision=args.revision)\n # .eval() on the wrapper too — it is a fresh nn.Module and defaults to training mode,\n # which the exporter warns about and which changes dropout/batchnorm behaviour.\n wrapper = DetectionWrapper(model).eval()\n\n out_dir = Path(args.output_dir)\n out_dir.mkdir(parents=True, exist_ok=True)\n onnx_path = out_dir / \"model.onnx\"\n\n # Neither backend handles every architecture. Try dynamo first (it copes with models the\n # tracer chokes on), then fall back to TorchScript when dynamo specializes the batch\n # dimension — which is what RT-DETR, the default model, does.\n strategies = [(\"legacy-torchscript\", export_torchscript)] if args.legacy_torchscript \\\n else [(\"dynamo\", export_dynamo), (\"legacy-torchscript\", export_torchscript)]\n\n for index, (name, export_fn) in enumerate(strategies):\n last = index == len(strategies) - 1\n print(f\"[export] backend={name}\")\n try:\n export_fn(wrapper, h, w, onnx_path, args.opset, args.static_batch, args.max_batch)\n except Exception as exc: # noqa: BLE001 - report and try the next backend\n if last:\n sys.exit(f\"ERROR: {name} export failed: {exc}\")\n print(f\"[export] {name} failed ({type(exc).__name__}); trying the next backend\")\n continue\n\n consolidate_external_data(onnx_path)\n if args.static_batch or verify(onnx_path):\n break\n if last:\n sys.exit(\n \"ERROR: every backend baked in a static batch dimension. Re-run with \"\n \"--static-batch and build a fixed-batch engine for this model.\"\n )\n print(f\"[export] {name} produced a static batch dimension; trying the next backend\")\n\n size_mb = onnx_path.stat().st_size / (1024 * 1024)\n print(f\"[export] wrote {onnx_path} ({size_mb:.1f} MB)\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10282,"content_sha256":"d71184514da06026547bfc245cdb5d3d80001cc9aacd0eb547abb4346f1df59c"},{"filename":"scripts/model/safetensors-to-onnx.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n################################################################################\n# Step 1 (alternate): Convert SafeTensors model to ONNX.\n#\n# Uses the shared venv's torch.onnx.export via safetensors_to_onnx.py. The former\n# optimum-cli path was removed: optimum held transformers below 4.54.0, which blocked\n# the releases that fix its RCE advisories, and optimum 2.1.0 dropped the `onnx`\n# subcommand outright.\n#\n# Writes \u003coutput_dir>/model.onnx — the same filename optimum produced.\n#\n# Usage: ./safetensors-to-onnx.sh \u003chf_model_id_or_path> \u003coutput_dir> [extra args]\n# Extra args are passed through to safetensors_to_onnx.py, e.g.\n# --opset 17 · --image-size 640 · --max-batch 16 · --static-batch\n# Examples:\n# ./safetensors-to-onnx.sh PekingU/rtdetr_r50vd ./onnx_export\n# ./safetensors-to-onnx.sh facebook/detr-resnet-50 ./onnx_export --opset 17\n# ./safetensors-to-onnx.sh ./local_model_dir ./onnx_export --image-size 800\n################################################################################\nset -euo pipefail\n\nif [ $# -lt 2 ]; then\n echo \"Usage: $0 \u003chf_model_id_or_path> \u003coutput_dir> [extra args for safetensors_to_onnx.py]\"\n echo \"\"\n echo \"Examples:\"\n echo \" $0 PekingU/rtdetr_r50vd ./onnx_export\"\n echo \" $0 facebook/detr-resnet-50 ./onnx_export --opset 17\"\n exit 1\nfi\n\nMODEL=\"$1\"\nOUTPUT_DIR=\"$2\"\nshift 2\nEXTRA_ARGS=(\"$@\")\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n# Installed layout is \u003croot>/.claude/skills/deepstream-import-vision-model/scripts/model/, so the\n# working root (where setup.sh created build/.venv_optimum) is five levels up from this script.\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../../../../..\" && pwd)\"\nmkdir -p \"$REPO_ROOT/build\"\nVENV_DIR=\"$REPO_ROOT/build/.venv_optimum\"\nVENV_PY=\"$VENV_DIR/bin/python\"\n\necho \"=== SafeTensors → ONNX Export ===\"\necho \"Model: $MODEL\"\necho \"Output dir: $OUTPUT_DIR\"\necho \"Extra args: ${EXTRA_ARGS[*]-}\"\necho \"Venv: $VENV_DIR\"\necho \"\"\n\n# The venv is built ONCE, IN THE CONTAINER, by setup.sh — with virtualenv, because the DeepStream\n# container's python lacks ensurepip (so `python3 -m venv` would fail here). Reuse it; never recreate.\nif [ ! -x \"$VENV_PY\" ]; then\n echo \"ERROR: venv not found at $VENV_DIR — run the one-time bootstrap first (in-container):\" >&2\n echo \" docker run --rm -it --gpus all --shm-size=16g -v \\\"\\$PWD\\\":/work -w /work \\\\\" >&2\n echo \" --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\\\\" >&2\n echo \" .claude/skills/deepstream-import-vision-model/setup.sh\" >&2\n exit 1\nfi\necho \"Using venv: $VENV_DIR\"\necho \"\"\n\n# ${arr[@]+\"${arr[@]}\"} expands to nothing when the array is empty. Plain \"${arr[@]-}\"\n# would pass a single empty-string argument instead, which argparse rejects.\n\"$VENV_PY\" \"$SCRIPT_DIR/safetensors_to_onnx.py\" \\\n --model \"$MODEL\" --output-dir \"$OUTPUT_DIR\" ${EXTRA_ARGS[@]+\"${EXTRA_ARGS[@]}\"}\n\necho \"\"\necho \"=== Export Complete ===\"\nls -lh \"$OUTPUT_DIR\"/*.onnx 2>/dev/null\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3664,"content_sha256":"d7bc07ce89350a3e7151c8b60a60bd83e42b9ef77d210e08d592c1034c3c1414"},{"filename":"scripts/preflight.sh","content":"#!/usr/bin/env bash\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\n# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.\n# Licensed under the Apache License, Version 2.0 (the \"License\").\n#\n# Preflight for deepstream-import-vision-model: verify the environment BEFORE running the\n# import loop. Everything runs THROUGH the DeepStream container (no host packages). Checks:\n# 1. docker daemon reachable (host-mode only)\n# 2. the DeepStream image is pulled (host-mode only)\n# 3. GPU visible inside the container\n# 4. the venv has the ONNX-export + report packages, and trtexec is present\n#\n# Usage:\n# Host (Linux): bash scripts/preflight.sh\n# Through Docker (any OS — the portable way):\n# docker run --rm --gpus all -v \"$PWD\":/work -w /work --entrypoint bash \\\n# nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\\n# .claude/skills/deepstream-import-vision-model/scripts/preflight.sh\nset -u\nIMG=\"${1:-nvcr.io/nvidia/deepstream:9.1-triton-multiarch}\"\nVENV=\"${2:-build/.venv_optimum}\"\nPKGS=\"torch torchvision transformers onnx onnxruntime onnxscript huggingface_hub matplotlib numpy markdown reportlab\"\nfail=0\nok(){ echo \" [OK] $1\"; }\nwarn(){ echo \" [WARN] $1\"; }\nbad(){ echo \" [MISSING] $1\"; fail=1; }\n\n# Container-mode: when run INSIDE the container (Windows/macOS invoke it as\n# docker run --gpus all -v \u003cpwd>:/work -w /work \u003cimage> bash scripts/preflight.sh\n# ), the host docker/image checks are moot and there's no nested docker CLI — verify GPU +\n# venv + trtexec DIRECTLY. On a Linux host (no /.dockerenv) fall through to host-orchestration.\nif [ -f /.dockerenv ]; then\n echo \"[preflight] container-mode (inside the container — verifying GPU + venv + trtexec directly)\"\n echo \"[preflight] 1/3 GPU (nvidia-smi)\"\n nvidia-smi -L >/dev/null 2>&1 && ok \"GPU visible in container\" \\\n || bad \"no GPU in container — run with --gpus all (Windows: Docker Desktop WSL2 backend + NVIDIA driver)\"\n echo \"[preflight] 2/3 trtexec\"\n [ -x /usr/src/tensorrt/bin/trtexec ] && ok \"trtexec present (/usr/src/tensorrt/bin/trtexec)\" \\\n || bad \"trtexec not found — is this the DeepStream/TensorRT image?\"\n echo \"[preflight] 3/3 python packages in $VENV\"\n if [ -x \"$VENV/bin/python\" ]; then\n miss=$(\"$VENV/bin/python\" -c \"import importlib.util as u;print(' '.join(p for p in '$PKGS'.split() if u.find_spec(p) is None))\" 2>/dev/null)\n if [ -z \"${miss// /}\" ]; then ok \"all packages present\"\n else bad \"missing in venv: $miss (run setup.sh to (re)build the venv)\"; fi\n else\n bad \"venv not found: $VENV/bin/python — run setup.sh first (creates build/.venv_optimum)\"\n fi\n echo \"[preflight] RESULT: $([ $fail -eq 0 ] && echo 'PASS — environment ready' || echo 'FAIL — fix [MISSING] items before running')\"\n exit $fail\nfi\n\necho \"[preflight] 1/4 docker daemon\"\ndocker info >/dev/null 2>&1 && ok \"docker reachable\" || bad \"docker not installed or daemon not running\"\n\necho \"[preflight] 2/4 DeepStream image: $IMG\"\nhave_img=0\nif docker image inspect \"$IMG\" >/dev/null 2>&1; then ok \"image present\"; have_img=1\nelse warn \"image not pulled — run: docker pull $IMG\"; fi\n\nif [ \"$have_img\" = 1 ]; then\n echo \"[preflight] 3/4 GPU via docker --gpus all\"\n docker run --rm --gpus all --entrypoint nvidia-smi \"$IMG\" -L >/dev/null 2>&1 \\\n && ok \"GPU visible in container\" \\\n || bad \"no GPU through --gpus all (check driver + nvidia-container-toolkit)\"\n\n echo \"[preflight] 4/4 python packages in $VENV\"\n if [ -x \"$VENV/bin/python\" ]; then\n miss=$(docker run --rm --entrypoint /work/\"$VENV\"/bin/python -v \"$PWD\":/work \"$IMG\" \\\n -c \"import importlib.util as u;print(' '.join(p for p in '$PKGS'.split() if u.find_spec(p) is None))\" 2>/dev/null)\n if [ -z \"${miss// /}\" ]; then ok \"all packages present\"\n else bad \"missing in venv: $miss (run setup.sh)\"; fi\n else\n bad \"venv not found: $VENV/bin/python — run setup.sh first\"\n fi\nelse\n echo \"[preflight] 3/4 GPU — SKIPPED (image not pulled)\"\n echo \"[preflight] 4/4 python packages — SKIPPED (image not pulled)\"\nfi\n\necho \"[preflight] RESULT: $([ $fail -eq 0 ] && echo 'PASS — environment ready' || echo 'FAIL — fix [MISSING] items before running')\"\nexit $fail\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4316,"content_sha256":"b04f3df52fd55b15fd8341bfcada1efe733e9ac23253090215b645999199382d"},{"filename":"scripts/report/generate-benchmark-charts.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nStep 8: Generate exactly 5 benchmark charts as PNG images for the report.\n\nUsage: python3 generate-benchmark-charts.py \u003coutput_dir> \u003cjson_data_file>\n\nExpected JSON format (benchmark_data.json written by report-generation skill pre-flight):\n{\n \"model_name\": \"yolo26_nano\",\n \"engine\": \"models/yolo26_nano/benchmarks/engines/yolo26n_dynamic_b256.engine\",\n \"max_bs\": 256,\n \"trtexec\": {\n \"bs1\": {\"qps\": 220.5, \"gpu_mean_ms\": 4.53},\n \"bsmax\": {\"qps\": 39.2, \"gpu_mean_ms\": 103.7, \"p99_ms\": 105.1, \"imgs_per_sec\": 10035.2}\n },\n \"peak_gpu_streams\": 334,\n \"deepstream\": {\n \"run1\": {\"streams\": 334, \"total_fps\": 7850.0, \"fps_per_stream\": 23.5},\n \"run2\": {\"streams\": 238, \"total_fps\": 7378.4, \"fps_per_stream\": 31.0}\n }\n}\n\nOutputs (fixed names — do not rename):\n chart_trtexec_bs1_vs_bsmax.png — grouped bar: QPS BS=1 vs BS=MAX_BS\n chart_trtexec_throughput.png — bar: imgs/sec at MAX_BS + PEAK_GPU_STREAMS annotation\n chart_ds_streams_vs_fps.png — line: stream count vs fps/stream, 30fps threshold\n chart_trt_vs_ds.png — grouped bar: trtexec vs DS Run1 vs DS Run2 total imgs/s\n chart_efficiency.png — bar: DS Run1 and Run2 pipeline efficiency %\n\"\"\"\nimport sys\nimport json\nimport os\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\n\nplt.rcParams.update({\n 'figure.facecolor': 'white',\n 'axes.facecolor': '#FAFAFA',\n 'axes.grid': True,\n 'grid.alpha': 0.3,\n 'font.size': 11,\n 'axes.titlesize': 13,\n 'axes.titleweight': 'bold',\n})\n\nCOLORS = {\n 'blue': '#2196F3',\n 'green': '#4CAF50',\n 'orange': '#FF9800',\n 'pink': '#E91E63',\n 'purple': '#9C27B0',\n 'teal': '#00BCD4',\n 'red': '#FF5722',\n}\n\n\ndef two_line_title(model_name, subtitle):\n \"\"\"Two-line title: model name (line 1) + subtitle (line 2).\"\"\"\n return f'{model_name}\\n{subtitle}'\n\n\ndef chart_trtexec_bs1_vs_bsmax(data, output_dir):\n \"\"\"Grouped bar chart: QPS at BS=1 vs BS=MAX_BS side by side.\"\"\"\n max_bs = data['max_bs']\n qps_bs1 = data['trtexec']['bs1']['qps']\n qps_bsmax = data['trtexec']['bsmax']['qps']\n\n labels = ['BS=1', f'BS={max_bs}']\n values = [qps_bs1, qps_bsmax]\n colors = [COLORS['blue'], COLORS['green']]\n\n fig, ax = plt.subplots(figsize=(10, 6))\n bars = ax.bar(labels, values, color=colors, width=0.5, edgecolor='white', linewidth=1.5)\n for bar, val in zip(bars, values):\n ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + max(values) * 0.01,\n f'{val:.1f}', ha='center', va='bottom', fontweight='bold', fontsize=13)\n ax.set_ylabel('QPS (queries/sec)', fontsize=13)\n ax.set_ylim(0, max(values) * 1.18)\n ax.grid(axis='y', alpha=0.3)\n ax.set_title(two_line_title(data['model_name'], f'trtexec QPS: BS=1 vs BS={max_bs}'))\n plt.tight_layout()\n out = os.path.join(output_dir, 'chart_trtexec_bs1_vs_bsmax.png')\n fig.savefig(out, dpi=150)\n plt.close(fig)\n print(f' chart_trtexec_bs1_vs_bsmax.png')\n\n\ndef chart_trtexec_throughput(data, output_dir):\n \"\"\"Single bar: GPU-only imgs/sec at MAX_BS with PEAK_GPU_STREAMS annotation.\"\"\"\n max_bs = data['max_bs']\n imgs_per_sec = data['trtexec']['bsmax']['imgs_per_sec']\n peak_streams = data['peak_gpu_streams']\n realtime_imgs = peak_streams * 30 # the throughput that satisfies peak_streams at 30fps\n\n fig, ax = plt.subplots(figsize=(10, 6))\n bar = ax.bar([f'BS={max_bs}'], [imgs_per_sec], color=COLORS['blue'], width=0.4,\n edgecolor='white', linewidth=1.5)\n ax.text(bar[0].get_x() + bar[0].get_width() / 2, imgs_per_sec + imgs_per_sec * 0.01,\n f'{imgs_per_sec:.0f}', ha='center', va='bottom', fontweight='bold', fontsize=13)\n\n # Annotation line at PEAK_GPU_STREAMS × 30fps threshold\n ax.axhline(y=realtime_imgs, color=COLORS['red'], linestyle='--', linewidth=2,\n label=f'PEAK_GPU_STREAMS={peak_streams} × 30fps = {realtime_imgs:.0f} imgs/s')\n ax.text(0.98, realtime_imgs + imgs_per_sec * 0.01,\n f'PEAK={peak_streams} streams',\n ha='right', va='bottom', color=COLORS['red'], fontsize=10, fontweight='bold',\n transform=ax.get_yaxis_transform())\n\n ax.set_ylabel('Images / sec', fontsize=13)\n ax.set_ylim(0, imgs_per_sec * 1.25)\n ax.grid(axis='y', alpha=0.3)\n ax.legend(loc='upper left', fontsize=10)\n ax.set_title(two_line_title(data['model_name'],\n f'GPU Throughput at BS={max_bs} (PEAK_GPU_STREAMS={peak_streams})'))\n plt.tight_layout()\n out = os.path.join(output_dir, 'chart_trtexec_throughput.png')\n fig.savefig(out, dpi=150)\n plt.close(fig)\n print(f' chart_trtexec_throughput.png')\n\n\ndef chart_ds_streams_vs_fps(data, output_dir):\n \"\"\"Line chart: X=stream count, Y=fps/stream. Red dashed line at 30fps.\"\"\"\n run1 = data['deepstream']['run1']\n run2 = data['deepstream']['run2']\n\n stream_counts = [run1['streams'], run2['streams']]\n fps_vals = [run1['fps_per_stream'], run2['fps_per_stream']]\n\n # Sort by stream count ascending\n pairs = sorted(zip(stream_counts, fps_vals))\n stream_counts = [p[0] for p in pairs]\n fps_vals = [p[1] for p in pairs]\n\n fig, ax = plt.subplots(figsize=(10, 6))\n ax.plot(stream_counts, fps_vals, color=COLORS['blue'], linewidth=2.5,\n marker='o', markersize=10, zorder=4)\n for sc, fp in zip(stream_counts, fps_vals):\n ax.text(sc, fp + max(fps_vals) * 0.025,\n f'{fp:.1f} fps', ha='center', va='bottom', fontweight='bold', fontsize=12)\n\n ax.axhline(y=30, color=COLORS['red'], linestyle='--', linewidth=2,\n label='30 fps/stream real-time threshold')\n\n ax.set_xlabel('Stream Count', fontsize=13)\n ax.set_ylabel('FPS / Stream', fontsize=13)\n lower = -max(fps_vals) * 0.15\n ax.set_ylim(lower, max(fps_vals) * 1.3)\n ax.set_xticks(stream_counts)\n ax.grid(axis='y', alpha=0.3)\n ax.legend(loc='upper right', fontsize=10)\n\n # Label each point\n run_labels = {run1['streams']: 'Run 1\\n(PEAK_GPU_STREAMS)',\n run2['streams']: 'Run 2\\n(RT_STREAMS)'}\n for sc in stream_counts:\n ax.annotate(run_labels.get(sc, ''), xy=(sc, 0), xytext=(sc, -max(fps_vals) * 0.12),\n ha='center', fontsize=9, color='#555555')\n\n ax.set_title(two_line_title(data['model_name'], 'DeepStream: FPS/Stream vs Stream Count'))\n plt.tight_layout()\n out = os.path.join(output_dir, 'chart_ds_streams_vs_fps.png')\n fig.savefig(out, dpi=150)\n plt.close(fig)\n print(f' chart_ds_streams_vs_fps.png')\n\n\ndef chart_trt_vs_ds(data, output_dir):\n \"\"\"Grouped bars: trtexec total imgs/s | DS Run 1 total imgs/s | DS Run 2 total imgs/s.\"\"\"\n max_bs = data['max_bs']\n trt_imgs = data['trtexec']['bsmax']['imgs_per_sec']\n ds1_imgs = data['deepstream']['run1']['total_fps']\n ds2_imgs = data['deepstream']['run2']['total_fps']\n n1 = data['deepstream']['run1']['streams']\n n2 = data['deepstream']['run2']['streams']\n\n labels = [f'trtexec\\nBS={max_bs}', f'DS Run 1\\n({n1} streams)', f'DS Run 2\\n({n2} streams)']\n values = [trt_imgs, ds1_imgs, ds2_imgs]\n colors = [COLORS['pink'], COLORS['blue'], COLORS['green']]\n\n fig, ax = plt.subplots(figsize=(10, 6))\n bars = ax.bar(labels, values, color=colors, width=0.5, edgecolor='white', linewidth=1.5)\n for bar, val in zip(bars, values):\n ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + max(values) * 0.01,\n f'{val:.0f}', ha='center', va='bottom', fontweight='bold', fontsize=13)\n ax.set_ylabel('Total Images / sec', fontsize=13)\n ax.set_ylim(0, max(values) * 1.18)\n ax.grid(axis='y', alpha=0.3)\n ax.set_title(two_line_title(data['model_name'], 'trtexec vs DeepStream: Total Throughput'))\n plt.tight_layout()\n out = os.path.join(output_dir, 'chart_trt_vs_ds.png')\n fig.savefig(out, dpi=150)\n plt.close(fig)\n print(f' chart_trt_vs_ds.png')\n\n\ndef chart_efficiency(data, output_dir):\n \"\"\"Bar chart: DS Run 1 and Run 2 pipeline efficiency %, dashed line at 100%.\"\"\"\n trt_imgs = data['trtexec']['bsmax']['imgs_per_sec']\n ds1_imgs = data['deepstream']['run1']['total_fps']\n ds2_imgs = data['deepstream']['run2']['total_fps']\n n1 = data['deepstream']['run1']['streams']\n n2 = data['deepstream']['run2']['streams']\n\n if trt_imgs \u003c= 0:\n print(\"ERROR: trtexec imgs_per_sec is zero or negative — cannot compute efficiency\", file=sys.stderr)\n sys.exit(1)\n eff1 = round(ds1_imgs / trt_imgs * 100, 1)\n eff2 = round(ds2_imgs / trt_imgs * 100, 1)\n\n labels = [f'DS Run 1\\n({n1} streams)', f'DS Run 2\\n({n2} streams)']\n values = [eff1, eff2]\n colors = [COLORS['purple'], COLORS['teal']]\n\n fig, ax = plt.subplots(figsize=(10, 6))\n bars = ax.bar(labels, values, color=colors, width=0.4, edgecolor='white', linewidth=1.5)\n ax.axhline(y=100, color='#333333', linestyle='--', linewidth=1.5, alpha=0.6,\n label='100% efficiency')\n for bar, val in zip(bars, values):\n ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,\n f'{val}%', ha='center', va='bottom', fontweight='bold', fontsize=13)\n ax.set_ylabel('DS Efficiency (%)', fontsize=13)\n ax.set_ylim(0, max(values) * 1.2)\n ax.grid(axis='y', alpha=0.3)\n ax.legend(loc='upper right', fontsize=10)\n ax.set_title(two_line_title(data['model_name'], 'DeepStream Pipeline Efficiency vs trtexec'))\n plt.tight_layout()\n out = os.path.join(output_dir, 'chart_efficiency.png')\n fig.savefig(out, dpi=150)\n plt.close(fig)\n print(f' chart_efficiency.png')\n\n\ndef main():\n if len(sys.argv) != 3:\n print(f\"Usage: {sys.argv[0]} \u003coutput_dir> \u003cjson_data_file>\")\n sys.exit(1)\n\n output_dir = sys.argv[1]\n json_file = sys.argv[2]\n\n os.makedirs(output_dir, exist_ok=True)\n\n with open(json_file) as f:\n data = json.load(f)\n\n model = data.get('model_name', 'unknown')\n print(f\"Generating 5 charts for {model} -> {output_dir}/\")\n chart_trtexec_bs1_vs_bsmax(data, output_dir)\n chart_trtexec_throughput(data, output_dir)\n chart_ds_streams_vs_fps(data, output_dir)\n chart_trt_vs_ds(data, output_dir)\n chart_efficiency(data, output_dir)\n print(\"Done — 5 charts written.\")\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11096,"content_sha256":"3952fdafe72e42a9f3462e90b2b55afb04be36241b2dba3ad1e1d22a0095b810"},{"filename":"scripts/report/md-to-html-pdf.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nConvert a GFM-style Markdown benchmark report to a styled HTML file and then\nto PDF via wkhtmltopdf.\n\nImages referenced as ![alt](file.png) are resolved relative to the markdown\nfile's directory and embedded as base64 data URIs so the HTML is self-contained.\n\nUsage:\n python3 md-to-html-pdf.py \u003creport.md> \u003cstyle.css> \u003coutput_dir> [model_name]\n\n model_name (optional): if provided, PDF is named benchmark_report_{model_name}.pdf\n if omitted, derived from output_dir parent folder name\n\nProduces:\n \u003coutput_dir>/benchmark_report.html\n \u003coutput_dir>/benchmark_report_{model_name}.pdf\n\"\"\"\nimport sys\nimport os\nimport re\nimport base64\nimport shutil\nimport subprocess\nimport markdown\n\ndef ensure_wkhtmltopdf():\n \"\"\"wkhtmltopdf is installed by setup.sh via apt, but that runs in an ephemeral\n --rm container so the binary does NOT persist to later per-phase containers\n (only the /work-mounted venv does). If it is missing, install it here so the\n PDF step works on a fresh run without a manual reinstall.\"\"\"\n if shutil.which('wkhtmltopdf'):\n return True\n apt = shutil.which('apt-get')\n if not apt:\n return False\n sudo = [] if os.geteuid() == 0 else (['sudo'] if shutil.which('sudo') else [])\n # Minimal environment rather than a copy of os.environ: apt-get needs only PATH and\n # HOME, and forwarding the full environment into a (possibly sudo-elevated) child\n # would carry loader hooks and apt overrides such as LD_PRELOAD or APT_CONFIG.\n env = {\n 'PATH': os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'),\n 'HOME': os.environ.get('HOME', '/root'),\n 'DEBIAN_FRONTEND': 'noninteractive',\n }\n print(\" wkhtmltopdf missing — installing (in-container, one-time per container)...\")\n for cmd in ([apt, 'update', '-qq'], [apt, 'install', '-y', '-qq', 'wkhtmltopdf']):\n subprocess.run(sudo + cmd, env=env,\n stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)\n return shutil.which('wkhtmltopdf') is not None\n\ndef embed_images(html: str, base_dir: str) -> str:\n \"\"\"Replace \u003cimg src=\"file.png\"> with base64-embedded data URIs.\"\"\"\n def replacer(match):\n prefix = match.group(1)\n src = match.group(2)\n suffix = match.group(3)\n # Skip URLs and absolute paths\n if re.match(r'^(https?|data|ftp)://', src) or os.path.isabs(src):\n return match.group(0)\n img_path = os.path.realpath(os.path.join(base_dir, src))\n base_real = os.path.realpath(base_dir)\n # Reject path traversal outside base_dir\n if not img_path.startswith(base_real + os.sep) and img_path != base_real:\n return match.group(0)\n if os.path.isfile(img_path):\n ext = os.path.splitext(src)[1].lstrip('.').lower()\n mime = {'png': 'image/png', 'jpg': 'image/jpeg',\n 'jpeg': 'image/jpeg', 'svg': 'image/svg+xml',\n 'gif': 'image/gif'}.get(ext, 'image/png')\n with open(img_path, 'rb') as f:\n b64 = base64.b64encode(f.read()).decode()\n return f'{prefix}data:{mime};base64,{b64}{suffix}'\n return match.group(0)\n return re.sub(r'(\u003cimg\\s[^>]*src=[\"\\'])([^\"\\']+)([\"\\'])', replacer, html)\n\ndef main():\n if len(sys.argv) not in (4, 5):\n print(f\"Usage: {sys.argv[0]} \u003creport.md> \u003cstyle.css> \u003coutput_dir> [model_name]\")\n sys.exit(1)\n\n md_path = sys.argv[1]\n css_path = sys.argv[2]\n out_dir = sys.argv[3]\n os.makedirs(out_dir, exist_ok=True)\n\n # Derive model name: explicit arg > parent-of-output_dir > \"model\"\n if len(sys.argv) == 5:\n model_name = sys.argv[4]\n else:\n # output_dir is typically models/{model_name}/reports/ — walk up two levels\n abs_out = os.path.abspath(out_dir)\n model_name = os.path.basename(os.path.dirname(abs_out)) or \"model\"\n\n base_dir = os.path.dirname(os.path.abspath(md_path))\n\n with open(md_path, encoding='utf-8') as f:\n md_text = f.read()\n\n # Strip YAML frontmatter\n md_text = re.sub(r'^---\\n.*?\\n---\\n', '', md_text, count=1, flags=re.DOTALL)\n\n with open(css_path, encoding='utf-8') as f:\n css = f.read()\n\n # Convert markdown to HTML\n html_body = markdown.markdown(md_text, extensions=['tables', 'fenced_code'])\n\n # Wrap in full HTML document\n html = f\"\"\"\u003c!DOCTYPE html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003ctitle>DeepStream Benchmark Report — {model_name}\u003c/title>\n\u003cstyle>\n{css}\n@media print {{\n body {{ max-width: 100%; padding: 10px; }}\n img {{ max-width: 100%; page-break-inside: avoid; }}\n table {{ page-break-inside: avoid; }}\n h2 {{ page-break-after: avoid; }}\n}}\n\u003c/style>\n\u003c/head>\n\u003cbody>\n{html_body}\n\u003c/body>\n\u003c/html>\"\"\"\n\n # Embed images as base64\n html = embed_images(html, base_dir)\n\n html_out = os.path.join(out_dir, 'benchmark_report.html')\n pdf_out = os.path.join(out_dir, f'benchmark_report_{model_name}.pdf')\n\n with open(html_out, 'w', encoding='utf-8') as f:\n f.write(html)\n print(f\" HTML: {html_out}\")\n\n # Ensure the PDF renderer is available (self-heal on fresh containers).\n if not ensure_wkhtmltopdf():\n print(\" PDF generation skipped: wkhtmltopdf unavailable and could not be installed. \"\n \"HTML report is complete.\", file=sys.stderr)\n sys.exit(1)\n\n # Convert to PDF.\n # Intentionally NOT passing --enable-local-file-access: all images have already\n # been converted to base64 data: URIs by embed_images(), and the CSS is inlined\n # in \u003cstyle>...\u003c/style>, so no file:// fetching is needed. Keeping it disabled\n # blocks a CSS/HTML-injection exfil vector if the upstream Markdown ever carries\n # untrusted content (e.g. an HF model card).\n result = subprocess.run(\n [\n 'wkhtmltopdf',\n '--page-size', 'A4',\n '--margin-top', '15mm',\n '--margin-bottom', '15mm',\n '--margin-left', '15mm',\n '--margin-right', '15mm',\n '--image-quality', '100',\n '--no-outline',\n html_out, pdf_out,\n ],\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n shell=False,\n timeout=300,\n )\n\n if result.returncode == 0:\n print(f\" PDF: {pdf_out}\")\n else:\n print(f\" PDF generation failed: {result.stderr[:500]}\", file=sys.stderr)\n sys.exit(1)\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7236,"content_sha256":"04d6375ba722a2e4782b0f7a9145c637d2ed3b3d0aae5f4cc71e8dc25336d94e"},{"filename":"scripts/report/md-to-pdf.sh","content":"#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Convert GitHub-Flavored Markdown (with optional Mermaid diagrams) to PDF with\n# correct wrapping: listings for code, Lua filter for tables/inline paths, LaTeX header.\n#\n# Usage:\n# ./md-to-pdf.sh \u003csource.md> [output.pdf]\n# If output.pdf is omitted, writes \u003csource>.pdf next to the source file.\n#\n# Requires: mmdc (Mermaid CLI), pandoc, pdflatex, packages: listings, xcolor, ragged2e.\n#\n# Do NOT replace this with plain \"pandoc --highlight-style=...\" — highlighted Verbatim\n# boxes do not wrap long lines; --listings + latex-pdf-wrap.tex + pandoc-wrap-tables.lua are required.\nset -euo pipefail\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\nSRC_INPUT=\"${1:?Usage: $0 \u003cmarkdown.md> [output.pdf]}\"\nif [[ \"$SRC_INPUT\" != /* ]]; then\n SRC=\"$(cd \"$(dirname \"$SRC_INPUT\")\" && pwd)/$(basename \"$SRC_INPUT\")\"\nelse\n SRC=\"$SRC_INPUT\"\nfi\n[[ -f \"$SRC\" ]] || { echo \"error: file not found: $SRC\" >&2; exit 1; }\n\nSRC_DIR=\"$(dirname \"$SRC\")\"\n\nif [[ -n \"${2-}\" ]]; then\n OUT=\"$2\"\n if [[ \"$OUT\" != /* ]]; then\n OUT=\"$(pwd)/$OUT\"\n fi\nelse\n OUT=\"${SRC%.md}.pdf\"\nfi\n\nSTEM=\"$(basename \"$SRC\" .md)\"\nINTERMEDIATE=\"${SRC_DIR}/${STEM}._pdf.md\"\nIMG_DIR=\"${SRC_DIR}/mermaid_pdf/${STEM}\"\n\npython3 \"$SCRIPT_DIR/render-mermaid-for-pdf.py\" \\\n --img-dir \"$IMG_DIR\" \\\n \"$SRC\" \\\n \"$INTERMEDIATE\"\n\npandoc \"$INTERMEDIATE\" \\\n --from=gfm \\\n --lua-filter=\"$SCRIPT_DIR/pandoc-wrap-tables.lua\" \\\n --include-in-header=\"$SCRIPT_DIR/latex-pdf-wrap.tex\" \\\n --pdf-engine=pdflatex \\\n -V geometry:margin=1in \\\n --listings \\\n --resource-path=\"$SRC_DIR:$SCRIPT_DIR\" \\\n -o \"$OUT\"\n\necho \"Wrote $OUT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2272,"content_sha256":"ddeac58686835b41f2d0ccf047f075d152fb296c1c68b1d3fc3bd3da8f14c16b"},{"filename":"scripts/report/mermaid-puppeteer-root.json","content":"{\n \"args\": [\"--no-sandbox\", \"--disable-setuid-sandbox\", \"--disable-dev-shm-usage\"]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":86,"content_sha256":"896e2d8f3bd9e8622ed9c709fa9c597fbe3b98976f808b8eb22ddb0a715bdd53"},{"filename":"scripts/report/mermaid-puppeteer.json","content":"{\n \"args\": [\"--disable-dev-shm-usage\"]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":42,"content_sha256":"03ae81fddec97b7f37a4086f1accdf50bc4a8018d680d2de9b8f4dfd0fb60c0f"},{"filename":"scripts/report/pandoc-wrap-tables.lua","content":"-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n-- SPDX-License-Identifier: Apache-2.0\n--\n-- Licensed under the Apache License, Version 2.0 (the \"License\");\n-- you may not use this file except in compliance with the License.\n-- You may obtain a copy of the License at\n--\n-- http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n\n-- PDF/LaTeX fixes for pandoc: wrapped table columns + breakable long paths in \\texttt.\n-- listings + pdflatex choke on Unicode in code blocks; normalize before LaTeX.\n\nfunction CodeBlock(block)\n block.text = block.text\n :gsub(\"\\u{2019}\", \"'\")\n :gsub(\"\\u{2018}\", \"'\")\n :gsub(\"\\u{201c}\", '\"')\n :gsub(\"\\u{201d}\", '\"')\n :gsub(\"\\u{2014}\", \"--\")\n :gsub(\"\\u{2013}\", \"-\")\n :gsub(\"\\u{00d7}\", \" x \")\n :gsub(\"\\u{00a0}\", \" \")\n :gsub(\"\\u{2192}\", \"->\") -- Unicode arrow (listings + pdflatex)\n return block\nend\n\nfunction Table(tbl)\n local specs = tbl.colspecs\n if not specs or #specs == 0 then\n return tbl\n end\n local n = #specs\n local w = 1.0 / n\n for i, spec in ipairs(specs) do\n local align = spec[1]\n -- Second field: fraction of \\linewidth (pandoc LaTeX writer)\n tbl.colspecs[i] = { align, w }\n end\n return tbl\nend\n\n-- Long path-like inline code does not wrap in LaTeX \\texttt; add \\allowbreak after each /.\n-- (Inline Code has no .format; do not gate on el.format — nil ~= \"\" is true in Lua and would skip all.)\nfunction Code(el)\n local t = el.text\n if not t:find(\"/\", 1, true) then\n return nil\n end\n if #t \u003c 32 and not t:match(\"/home/\") and not t:match(\"%.sh\") then\n return nil\n end\n local out = t\n :gsub(\"\\\\\", \"\\\\textbackslash{}\")\n :gsub(\"_\", \"\\\\_\")\n :gsub(\"{\", \"\\\\{\")\n :gsub(\"}\", \"\\\\}\")\n :gsub(\"%$\", \"\\\\$\")\n :gsub(\"#\", \"\\\\#\")\n :gsub(\"%^\", \"\\\\textasciicircum{}\")\n :gsub(\"&\", \"\\\\&\")\n :gsub(\"%%\", \"\\\\%\")\n out = out:gsub(\"/\", \"/\\\\allowbreak \")\n return pandoc.RawInline(\"latex\", \"\\\\texttt{\" .. out .. \"}\")\nend\n","content_type":"text/plain; charset=utf-8","language":"lua","size":2286,"content_sha256":"b677ce07908bbd4a0c33df365e9b1c3d93c52c6f0daf82080cd41adc811425a7"},{"filename":"scripts/report/render-mermaid-for-pdf.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nExpand ```mermaid ... ``` blocks in a Markdown file into PNG images via mmdc,\nproducing a new .md suitable for pandoc -> PDF. Does not modify the source file.\n\nFull PDF pipeline (see docs/md-to-pdf.sh and docs/build-pdf.sh):\n 1. This script: Mermaid -> PNG under docs/mermaid_pdf/\u003cstem>/, replace blocks with ![...](...) links.\n 2. pandoc --from=gfm --listings --lua-filter=pandoc-wrap-tables.lua\n --include-in-header=latex-pdf-wrap.tex --pdf-engine=pdflatex\n\nUse --listings (not --highlight-style): default highlighted Verbatim splits code into\nunbreakable tokens and overflows the page. The Lua filter wraps pipe tables and long\npath-like inline code; CodeBlock text is normalized for pdflatex (Unicode quotes, etc.).\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport os\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nMERMAID_BLOCK = re.compile(\n r\"^```mermaid\\s*\\n(.*?)^```\\s*$\",\n re.MULTILINE | re.DOTALL,\n)\n\n\ndef render_one(\n mmdc: str,\n body: str,\n out_png: Path,\n width: int,\n scale: float,\n puppeteer_config: Path | None,\n) -> None:\n out_png.parent.mkdir(parents=True, exist_ok=True)\n tmp = out_png.with_suffix(\".mmd\")\n tmp.write_text(body.strip() + \"\\n\", encoding=\"utf-8\")\n cmd = [\n mmdc,\n \"-i\",\n str(tmp),\n \"-o\",\n str(out_png),\n \"-e\",\n \"png\",\n \"-b\",\n \"white\",\n \"-w\",\n str(width),\n \"-s\",\n str(scale),\n \"-q\",\n ]\n if puppeteer_config is not None:\n cmd.extend([\"-p\", str(puppeteer_config)])\n r = subprocess.run(\n cmd,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n shell=False,\n timeout=120,\n )\n tmp.unlink(missing_ok=True)\n if r.returncode != 0:\n sys.stderr.write(r.stderr or r.stdout or \"mmdc failed\\n\")\n raise RuntimeError(f\"mmdc failed with code {r.returncode}\")\n\n\ndef main() -> None:\n ap = argparse.ArgumentParser()\n ap.add_argument(\"source\", type=Path, help=\"Input .md path\")\n ap.add_argument(\"output\", type=Path, help=\"Output .md path\")\n ap.add_argument(\n \"--img-dir\",\n type=Path,\n default=None,\n help=\"Directory for PNGs (default: next to output, mermaid_pdf/)\",\n )\n ap.add_argument(\"--mmdc\", default=\"mmdc\", help=\"Path to mmdc binary\")\n ap.add_argument(\"--width\", type=int, default=1100)\n ap.add_argument(\"--scale\", type=float, default=1.5)\n ap.add_argument(\n \"--puppeteer-config\",\n type=Path,\n default=None,\n help=\"JSON for Puppeteer (default: mermaid-puppeteer.json next to this script)\",\n )\n args = ap.parse_args()\n # Optional: MERMAID_PDF_WIDTH / MERMAID_PDF_SCALE (e.g. build-pdf.sh for design doc)\n if os.environ.get(\"MERMAID_PDF_WIDTH\"):\n args.width = int(os.environ[\"MERMAID_PDF_WIDTH\"])\n if os.environ.get(\"MERMAID_PDF_SCALE\"):\n args.scale = float(os.environ[\"MERMAID_PDF_SCALE\"])\n\n script_dir = Path(__file__).resolve().parent\n\n # Two vetted Puppeteer configs ship alongside this script:\n # - mermaid-puppeteer.json : Chromium sandbox enabled. Used for\n # non-root execution (the secure\n # default for laptops, CI runners that\n # run as a non-root user, etc.).\n # - mermaid-puppeteer-root.json : --no-sandbox / --disable-setuid-sandbox.\n # Used only when this script runs as\n # uid 0, because Chromium refuses to\n # start with the setuid sandbox enabled\n # when running as root (common inside\n # container build environments).\n # Both configs also pass --disable-dev-shm-usage, which is a stability\n # workaround for small /dev/shm in containers (not a security flag).\n #\n # Selection is driven by the effective uid, never by user input. Any\n # --puppeteer-config that doesn't resolve to one of these two shipped\n # files is rejected. This prevents an attacker-supplied config from\n # introducing extra dangerous flags such as --remote-debugging-port\n # (would expose a control channel to the headless browser) or\n # --load-extension (would let arbitrary JS run in Chromium).\n sandboxed_pc = script_dir / \"mermaid-puppeteer.json\"\n root_pc = script_dir / \"mermaid-puppeteer-root.json\"\n\n is_root = hasattr(os, \"geteuid\") and os.geteuid() == 0\n default_pc = root_pc if is_root else sandboxed_pc\n\n allowed = {p.resolve() for p in (sandboxed_pc, root_pc) if p.exists()}\n if args.puppeteer_config is not None:\n requested = args.puppeteer_config.resolve()\n if requested not in allowed:\n sys.stderr.write(\n \"Refusing --puppeteer-config: only the shipped configs are \"\n f\"allowed ({sandboxed_pc.name}, {root_pc.name}). \"\n f\"Got: {requested}\\n\"\n )\n sys.exit(2)\n default_pc = args.puppeteer_config\n\n puppeteer_config = default_pc if default_pc.is_file() else None\n if puppeteer_config is not None:\n uid_str = str(os.geteuid()) if hasattr(os, \"geteuid\") else \"n/a\"\n sys.stderr.write(\n f\"[render-mermaid-for-pdf] using puppeteer config: \"\n f\"{puppeteer_config.name} (uid={uid_str})\\n\"\n )\n\n # Validate source path exists and is a regular file\n if not args.source.is_file():\n sys.stderr.write(f\"ERROR: source markdown not found: {args.source}\\n\")\n sys.exit(1)\n\n text = args.source.read_text(encoding=\"utf-8\")\n img_dir = args.img_dir\n if img_dir is None:\n img_dir = args.output.parent / \"mermaid_pdf\"\n\n n = 0\n\n out_parent = args.output.parent.resolve()\n\n def repl(m: re.Match[str]) -> str:\n nonlocal n\n n += 1\n body = m.group(1)\n png_name = f\"diagram_{n:02d}.png\"\n out_png = img_dir / png_name\n render_one(\n args.mmdc,\n body,\n out_png,\n args.width,\n args.scale,\n puppeteer_config,\n )\n try:\n rel_to_md = out_png.resolve().relative_to(out_parent)\n except ValueError:\n # --img-dir is outside the output directory; fall back to os.path.relpath\n rel_to_md = Path(os.path.relpath(out_png.resolve(), out_parent))\n return f\"\\n![Mermaid diagram {n}]({rel_to_md.as_posix()})\\n\"\n\n new_text, count = MERMAID_BLOCK.subn(repl, text)\n args.output.write_text(new_text, encoding=\"utf-8\")\n if count:\n print(f\"Rendered {count} Mermaid diagram(s) into {img_dir}\", file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7548,"content_sha256":"8c0f5f3584b9c13d1466747e2d843434b6244bc386a2547f778aa0da05eee1e3"},{"filename":"scripts/report/report-style.css","content":"/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */\n/* SPDX-License-Identifier: Apache-2.0 */\n\nbody {\n font-family: 'Segoe UI', Arial, Helvetica, sans-serif;\n font-size: 14px;\n line-height: 1.6;\n color: #1a1a1a;\n max-width: 900px;\n margin: 0 auto;\n padding: 20px;\n}\n\nh1 { color: #1a237e; border-bottom: 3px solid #1a237e; padding-bottom: 8px; }\nh2 { color: #283593; border-bottom: 2px solid #c5cae9; padding-bottom: 6px; margin-top: 30px; }\nh3 { color: #3949ab; margin-top: 20px; }\n\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 16px 0;\n font-size: 13px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n}\n\nthead tr {\n background-color: #283593;\n color: #ffffff;\n font-weight: bold;\n}\n\nth {\n border: 1px solid #1a237e;\n padding: 10px 12px;\n text-align: left;\n}\n\ntd {\n border: 1px solid #c5cae9;\n padding: 8px 12px;\n text-align: left;\n}\n\ntbody tr:nth-child(odd) {\n background-color: #e8eaf6;\n}\n\ntbody tr:nth-child(even) {\n background-color: #ffffff;\n}\n\ntbody tr:hover {\n background-color: #c5cae9;\n}\n\n/* Bold first column in tables */\ntd:first-child {\n font-weight: 600;\n color: #1a237e;\n}\n\ncode {\n background-color: #f5f5f5;\n border: 1px solid #e0e0e0;\n border-radius: 3px;\n padding: 1px 5px;\n font-size: 12px;\n}\n\npre {\n background-color: #263238;\n color: #eeffff;\n border-radius: 6px;\n padding: 14px;\n overflow-x: auto;\n font-size: 12px;\n line-height: 1.5;\n}\n\npre code {\n background: none;\n border: none;\n color: inherit;\n padding: 0;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n display: block;\n margin: 16px auto;\n border-radius: 4px;\n box-shadow: 0 2px 6px rgba(0,0,0,0.15);\n}\n\nblockquote {\n border-left: 4px solid #283593;\n background-color: #e8eaf6;\n padding: 10px 16px;\n margin: 16px 0;\n}\n\nstrong { color: #1a237e; }\n\nul, ol { margin: 8px 0; }\nli { margin: 4px 0; }\n","content_type":"text/css; charset=utf-8","language":"css","size":1983,"content_sha256":"9231631f966bfe9b76e55fc34f311dc688da646c97bd942cf6c4a1bf4ff3c271"},{"filename":"scripts/requirements.txt","content":"# Python dependencies for deepstream-import-vision-model (installed into build/.venv_optimum).\n# Run INSIDE nvcr.io/nvidia/deepstream:9.1-triton-multiarch via setup.sh — NOT on the host.\n# Release target: DeepStream 9.1 Triton image (Python 3.12); run the GPU validation matrix before signing.\n#\n# NOTE on torch: the default linux-x86_64 PyPI wheel for torch 2.13.0 bundles a CUDA 13 runtime\n# (nvidia-cudnn-cu13 / nccl-cu13 / cusparselt-cu13). It relies on NVIDIA driver compatibility rather\n# than the container CUDA toolkit; no special --index-url is needed. This replaces the previous\n# torch 2.6.0 / cu124 pin. 2.13.0 is the first torch release with zero open OSV advisories.\n# (Same torch/torchvision pins the sibling deepstream-eval-and-finetune skill uses.)\n#\n# VALIDATED end-to-end on 2026-08-04 in nvcr.io/nvidia/deepstream:9.1-triton-multiarch (H100):\n# setup.sh resolved and installed this exact set; SafeTensors -> ONNX -> TRT 10.16 FP16\n# dynamic-shape engine built and PASSED trtexec for both\n# PekingU/rtdetr_r50vd (default model) 65.0 qps, 15.37 ms\n# hustvl/yolos-tiny 171.9 qps, 5.81 ms\n# ONNX Runtime confirmed a working dynamic batch dimension at batch 1/3/8.\n# Still to confirm on the full release matrix: Jetson/aarch64 and the non-H100 GPU lineup.\n#\n# NOTE on the export path: SafeTensors -> ONNX goes through scripts/model/safetensors_to_onnx.py\n# (direct torch.onnx.export), NOT optimum-cli. optimum was removed because it pinned transformers\n# below 4.54.0 (and optimum-onnx below 4.58.0), while the transformers RCE (GHSA-29pf-2h5f-8g72)\n# is fixed only in 5.3.0 and the model-init arbitrary code execution (GHSA-fgcw-684q-jj6r) only in\n# 5.5.0 — so no transformers 4.x release cleared them. optimum 2.1.0 also removed the `onnx`\n# subcommand entirely, making that path a dead end regardless.\n\n# --- ONNX export (SafeTensors -> ONNX, via torch.onnx.export) ---\ntorch==2.13.0\ntorchvision==0.28.0 # torchvision 0.28.0 requires torch==2.13.0 exactly\ntransformers==5.14.1\nonnx==1.22.0\nonnxruntime==1.27.0\nonnxscript==0.7.0 # required by the torch.onnx.export dynamo backend\nhuggingface_hub==1.26.0 # transformers 5.14.1 requires >=1.5.0,\u003c2.0\ntokenizers==0.22.2 # highest STABLE release inside transformers' >=0.22.0,\u003c=0.23.0 range\n # (0.23.0 was never released as final; 0.23.1 exceeds the ceiling)\nsafetensors==0.8.0 # transformers 5.14.1 requires >=0.8.0\n\n# --- report: charts + markdown->HTML (PDF via wkhtmltopdf, apt-installed in the container by setup.sh) ---\nmatplotlib==3.10.9\nnumpy==1.26.4\nmarkdown==3.10.2\nreportlab==4.5.1\n","content_type":"text/plain; charset=utf-8","language":null,"size":2660,"content_sha256":"185472f0cd15a52f4df64f541cd555b650f1527e26b5f2f7678c8cc97abe57b6"},{"filename":"setup.sh","content":"#!/usr/bin/env bash\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\n# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Apache-2.0.\n#\n# One-command environment bootstrap for deepstream-import-vision-model on a FRESH machine.\n# Creates the shared venv (build/.venv_optimum), installs the ONNX-export + report Python deps,\n# and installs wkhtmltopdf (for the PDF report) — ALL INSIDE the container. Idempotent.\n#\n# Nothing runs on the host: the host only needs Docker + the NVIDIA driver. Run this INSIDE the\n# DeepStream container, from the working root (where models/, reports/, build/ will live) — so\n# torch/CUDA/TensorRT and the compiled parser all match the runtime:\n#\n# # Linux / WSL2 bash:\n# docker run --rm -it --gpus all --shm-size=16g -v \"$PWD\":/work -w /work \\\n# --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\\n# .claude/skills/deepstream-import-vision-model/setup.sh\n# # PowerShell: use -v \"${PWD}:/work\" · cmd: -v \"%cd%:/work\" (see references/windows.md)\nset -euo pipefail\n\nSK=\".claude/skills/deepstream-import-vision-model\"\nVENV=\"build/.venv_optimum\"\nPY=\"$VENV/bin/python\"\n\nif [ ! -d \"$SK\" ]; then\n echo \"[setup] ERROR: run from the working root (the dir that contains $SK).\" >&2\n exit 1\nfi\n\n# 1) virtualenv (the container python lacks ensurepip, so bootstrap virtualenv via pip)\nif [ ! -x \"$PY\" ]; then\n echo \"[setup] creating venv at $VENV\"\n python3 -m pip install --quiet --user virtualenv 2>/dev/null || python3 -m pip install --quiet virtualenv\n python3 -m virtualenv \"$VENV\"\nelse\n echo \"[setup] venv exists: $VENV\"\nfi\n\n# 2) Python dependencies (ONNX export + report)\necho \"[setup] installing Python deps from $SK/scripts/requirements.txt (this can take several minutes)\"\n\"$PY\" -m pip install --quiet --upgrade pip\n\"$PY\" -m pip install -r \"$SK/scripts/requirements.txt\"\n\n# 3) wkhtmltopdf for the PDF report — installed IN the container (not a host dependency).\n# Self-contained Qt-WebKit renderer; no browser/Chromium needed.\nif command -v wkhtmltopdf >/dev/null 2>&1; then\n echo \"[setup] wkhtmltopdf already present\"\nelse\n echo \"[setup] installing wkhtmltopdf (apt, in-container)\"\n if [ \"$(id -u)\" = \"0\" ]; then APT=\"\"; else APT=\"sudo\"; fi\n $APT apt-get update -qq && $APT apt-get install -y -qq wkhtmltopdf \\\n || echo \"[setup] WARN: wkhtmltopdf install failed — the HTML report still works; PDF step will skip\"\nfi\n\n# 4) verify\necho \"[setup] verifying…\"\n\"$PY\" - \u003c\u003c'PYV'\nimport importlib.util, sys\nmods = [\"torch\",\"torchvision\",\"transformers\",\"onnx\",\"onnxruntime\",\"onnxscript\",\n \"huggingface_hub\",\"matplotlib\",\"numpy\",\"markdown\",\"reportlab\"]\nmissing = [m for m in mods if importlib.util.find_spec(m) is None]\nimport torch\nprint(f\" torch {torch.__version__} cuda_available={torch.cuda.is_available()}\")\nif missing: print(\" MISSING:\", missing); sys.exit(1)\nprint(\" all required packages present\")\nPYV\ncommand -v wkhtmltopdf >/dev/null 2>&1 && echo \" wkhtmltopdf: $(command -v wkhtmltopdf)\" || echo \" wkhtmltopdf: (absent — PDF step will skip)\"\ncommand -v /usr/src/tensorrt/bin/trtexec >/dev/null 2>&1 && echo \" trtexec: /usr/src/tensorrt/bin/trtexec\" || echo \" trtexec: (check TensorRT in image)\"\n\necho\necho \"[setup] DONE. Next: run preflight, then the phases via the container —\"\necho \" docker run --rm --gpus all -v \\\"\\$PWD\\\":/work -w /work --entrypoint bash \\\\\"\necho \" nvcr.io/nvidia/deepstream:9.1-triton-multiarch $SK/scripts/preflight.sh\"\necho \" (see $SK/SKILL.md + $SK/references/windows.md for the per-shell mount token)\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3638,"content_sha256":"5f1304d3b313f46d7922e22d03970fcb8bec6e5cf541e4ab7946e6a351b490f8"},{"filename":"skill-card.md","content":"## Description: \u003cbr>\nUse this skill to bring a supported object-detection vision model from HuggingFace or NVIDIA NGC into an NVIDIA DeepStream pipeline with end-to-end automation: ONNX download, SafeTensors export, TRT engine build, custom nvinfer bbox parser, multi-stream benchmark, and PDF report. \u003cbr>\n\nThis skill is ready for commercial/non-commercial use. \u003cbr>\n\n## Owner\nNVIDIA \u003cbr>\n\n### License/Terms of Use: \u003cbr>\nCC-BY-4.0 AND Apache-2.0 \u003cbr>\n## Use Case: \u003cbr>\nDevelopers and engineers use this skill to import supported object-detection vision models from HuggingFace or NVIDIA NGC into NVIDIA DeepStream inference pipelines, automating the full workflow from model acquisition through TensorRT engine build, multi-stream benchmarking, and PDF report generation. \u003cbr>\n\n### Deployment Geography for Use: \u003cbr>\nGlobal \u003cbr>\n\n## Requirements / Dependencies: \u003cbr>\n**Requires API Key or External Credential:** [Not Specified] \u003cbr>\n**Credential Type(s):** [None identified] \u003cbr>\n\nDo not include secrets in prompts/logs/output; use least-privilege credentials; rotate keys as appropriate. \u003cbr>\n\n## Known Risks and Mitigations: \u003cbr>\nRisk: Review before execution as proposals could introduce incorrect or misleading guidance into skills. \u003cbr>\nMitigation: Review and scan skill before deployment. \u003cbr>\n\n## Reference(s): \u003cbr>\n- [Model Acquire](references/model-acquire.md) \u003cbr>\n- [Engine Build](references/engine-build.md) \u003cbr>\n- [Pipeline Run](references/pipeline-run.md) \u003cbr>\n- [Report Generation](references/report-generation.md) \u003cbr>\n- [Windows Support](references/windows.md) \u003cbr>\n\n\n## Skill Output: \u003cbr>\n**Output Type(s):** [Shell commands, Code, Files, Analysis] \u003cbr>\n**Output Format:** [TensorRT engines, ONNX models, C++ parser source, nvinfer configs, benchmark logs, PDF/HTML/Markdown reports] \u003cbr>\n**Output Parameters:** [1D] \u003cbr>\n**Other Properties Related to Output:** [Outputs organized in a mandatory directory structure under models/{model_name}/] \u003cbr>\n\n## Evaluation Agents Used: \u003cbr>\n- Claude Code (`aws/anthropic/bedrock-claude-opus-4-8`) \u003cbr>\n- Codex (`openai/openai/gpt-5.5`) \u003cbr>\n\n\n\n## Evaluation Tasks: \u003cbr>\n13 evaluation tasks (13 positive), each run in isolated sandbox pods. \u003cbr>\n\n## Evaluation Metrics Used: \u003cbr>\nReported benchmark dimensions: \u003cbr>\n- Security: Checks whether the skill is safe to use — detects unsafe operations, secret leakage, and unauthorized access. \u003cbr>\n- Correctness: Checks whether the final answer is correct against the reference answer. \u003cbr>\n- Discoverability: Checks whether the right skill was found and executed when needed. \u003cbr>\n- Effectiveness: Checks whether the skill helped complete the user's goal (goal completion and expected workflow adherence). \u003cbr>\n- Efficiency: Checks routing quality, workspace-aware skill reads, and productive tool use. \u003cbr>\n\nUnderlying evaluation signals used in this run: \u003cbr>\n- `security`: Detects unsafe operations, secret leakage, and unauthorized access. \u003cbr>\n- `skill_execution`: Verifies whether the expected skill was found and executed. \u003cbr>\n- `skill_efficiency`: Evaluates routing quality, workspace-aware skill reads, and productive tool use. \u003cbr>\n- `accuracy`: Measures final-answer correctness against the reference answer. \u003cbr>\n- `goal_accuracy`: Measures whether the user's goal was achieved. \u003cbr>\n- `behavior_check`: Verifies whether the expected workflow behavior was followed. \u003cbr>\n\n\n\n## Evaluation Results: \u003cbr>\n| Measure | Claude Code (Baseline → Skill Uplift) | Codex (Baseline → Skill Uplift) |\n|---|---:|---:|\n| Overall | 44% → 72% (+27 points) | 46% → 61% (+15 points) |\n| Security | 58% → 54% (-4 points) | 35% → 42% (+8 points) |\n| Correctness | 42% → 86% (+45 points) | 65% → 72% (+8 points) |\n| Discoverability | 51% → 88% (+37 points) | 50% → 72% (+22 points) |\n| Effectiveness | 24% → 43% (+19 points) | 28% → 34% (+6 points) |\n| Efficiency | 48% → 86% (+38 points) | 54% → 84% (+30 points) |\n\n## Skill Version(s): \u003cbr>\n1.5.2 (source: frontmatter) \u003cbr>\n\n## Ethical Considerations: \u003cbr>\nNVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal team to ensure this skill meets requirements for the relevant industry and use case and addresses unforeseen product misuse. \u003cbr>\n\n(For Release on NVIDIA Platforms Only) \u003cbr>\nPlease report quality, risk, security vulnerabilities or NVIDIA AI Concerns [here](https://app.intigriti.com/programs/nvidia/nvidiavdp/detail). \u003cbr>\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4631,"content_sha256":"80ce1c9d3342797a9309bd78d70e9c25f0cf08677902771aa0af37f70dd8350d"},{"filename":"tests/__init__.py","content":"# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n","content_type":"text/x-python; charset=utf-8","language":"python","size":137,"content_sha256":"39b67a3543de12fbe919327958818a53222b223688ef8ece33a12bc1e46d1f14"},{"filename":"tests/README.md","content":"# deepstream-import-vision-model tests\n\nUnit / validation tests for the hardened scripts in this skill. These run\nlocally with only `python3` (stdlib + `bash`) — no GPU, no network, no external\npackage install required.\n\n## What is covered\n\n| Test class | Script | What it verifies |\n|---|---|---|\n| `TestInstallScript` | `install.sh` | `--target` rejects `/`, `..`, empty, missing; dry-run previews the self-contained skill copy without writing to target |\n| `TestCleanupScript` | `scripts/model/cleanup.sh` | `MODEL_NAME` regex enforcement; shell meta-chars / slashes rejected; dry-run does not touch real files |\n| `TestHFScripts` | `scripts/model/hf-list-files.sh`, `hf-download-config.sh` | `HF_ORG` / `MODEL_NAME` / `DEST` validation |\n| `TestNGCScripts` | `scripts/model/ngc-list-files.sh`, `ngc-download.sh` | NGC arg validation; `DEST_DIR` refuses `\"\"`, `/`, path-traversal |\n| `TestKittiDumpUsage` | `scripts/deepstream/ds-kitti-dump.sh` | Usage message printed when required args missing |\n| `TestEmbedImages` | `scripts/report/md-to-html-pdf.py` | Local images inlined as `data:` URIs; remote / absolute / traversal paths left alone (proves `--enable-local-file-access` is safe to drop) |\n| `TestPowerShellInstaller` | `install.ps1` (**both** skills) | Every source file is installed — asserts an exact file-set match, not just that `SKILL.md` exists; Claude + Codex targets; `-NoCursor`; reinstall is idempotent and does not nest; the missing-`SKILL.md` guard throws |\n\n### Note on `TestPowerShellInstaller`\n\nThis one exists because of a shipped bug: `install.ps1` used\n`Copy-Item -LiteralPath $SkillDir -Destination $dest -Recurse`, whose behaviour depends on whether\nthe destination already exists, and which on **Windows PowerShell 5.1** created the directory tree\nwithout copying any files — an empty skill dir, no `SKILL.md`, and a skill the agent runtime\nsilently refused to load. Nothing caught it because the suites only ever exercised `install.sh`.\n\nIt needs a PowerShell, discovered in this order: `pwsh`, `powershell`, then the\n`mcr.microsoft.com/powershell` docker image if already pulled. With none available it **skips** —\nit will never fail on a machine without PowerShell. To enable it on a Linux dev box:\n\n```bash\ndocker pull mcr.microsoft.com/powershell:latest\n```\n\nCaveat: the dev-box/container runner is PowerShell **7**. The original bug was 5.1-specific, so\nthis pins the copy *contract* rather than reproducing every 5.1 quirk — run it on Windows for full\nfidelity.\n\n## Run\n\nFrom the skill root:\n\n```bash\npython3 -m unittest discover -s tests -v\n```\n\nOr a single class:\n\n```bash\npython3 -m unittest tests.test_hardened_scripts.TestInstallScript -v\n```\n\n## Adding tests\n\nTests shell out to the real scripts and assert on exit codes + stderr/stdout,\nor import Python helpers directly. Keep them hermetic: use `tempfile.TemporaryDirectory()`,\ndon't rely on network access, and don't shell out to tools that may not be\ninstalled on every dev machine (e.g. `trtexec`, `deepstream-app`, `wkhtmltopdf`,\n`mmdc`, `ngc`).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3058,"content_sha256":"5d9d7a06f069a986068ca90592b197b33846e59a2883c78b8a2bb5e96a175967"},{"filename":"tests/test_evals_contract.py","content":"# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\"\"\"Contract tests for ``evals/evals.json``.\n\nNothing in this repo validates the skill eval files, so every past defect had to be\ncaught by a human or by a remote NVSkills-Eval run. The 2026-08-12 Litmus dispatch\n(run 6beb1477-c4c2-416c-b28d-77314215aebb) reported two classes of defect that these\ntests pin down:\n\n* **Negation-blind guards** — a ``not_contains_pattern`` that matches a bare\n identifier fires when the response *names the thing it is declining to do*, so a\n correct refusal is graded as if it performed the forbidden action.\n* **Credential false positives** — ``[A-Za-z0-9+/]{40,}`` matched ordinary\n HuggingFace ``resolve/main/...`` URLs and the string ``token-classification``.\n\n``ANTI_NEGATION_CORPUS`` and ``BENIGN_STRINGS`` are the regression fixtures: every\nsentence in them is something a *correct* run legitimately emits, so no\n``not_contains_pattern`` in the suite may match any of them.\n\"\"\"\n\nimport json\nimport re\nimport unittest\nfrom pathlib import Path\n\n\nSKILL_ROOT = Path(__file__).resolve().parents[1]\nEVALS_PATH = SKILL_ROOT / \"evals\" / \"evals.json\"\n\nVALID_ASSERTION_TYPES = {\n \"contains_pattern\",\n \"not_contains_pattern\",\n \"contains_phrase\",\n}\n\n# Sentences a *correct* run emits. No negative guard may match any of these.\nANTI_NEGATION_CORPUS = [\n \"gpt2 is a language model; I will not run trtexec or an engine build for nvinfer.\",\n \"This is out of scope, so I will not build a TensorRT engine or start a DeepStream pipeline.\",\n \"Local path detected - skipping huggingface_hub download entirely.\",\n \"The model is already on disk, so hf_hub_download is not needed.\",\n \"Multiple variants found; I automatically choose model_fp16.onnx without prompting.\",\n \"I will not ask which variant to use - the fp16-first rule selects it deterministically.\",\n \"To clarify, I will run the full import pipeline now without asking which skill applies.\",\n \"No custom parser compilation is needed: the built-in TAO parser in \"\n \"libnvds_infercustomparser.so is reused, so no g++ or cmake invocation is required.\",\n \"ResNet-50 is a classifier, so I am not going to run deepstream-app or gst-launch-1.0.\",\n \"You do not need to install the SDK with trtexec; apt-get installs the deb.\",\n]\n\n# Strings the skill legitimately prints that must never be flagged as a credential.\nBENIGN_STRINGS = [\n \"https://huggingface.co/onnx-community/yolov8n/resolve/main/onnx/model_fp16.onnx\",\n \"The config.json pipeline_tag is token-classification, so this is rejected.\",\n \"Export HF_TOKEN in your environment before running (never hardcode it).\",\n \"dataset digest sha256:a14983294ef15eedf15e8e8e68ae18da7cde7f1720e6634896c5f7c8b6ddbdc7\",\n \"Set TOKENIZERS_PARALLELISM=false to silence the warning.\",\n \"engine models/rtdetr_r50vd_dynamic_b16.engine built with trtexec --fp16\",\n]\n\n# Credential-shaped probes the guard must still catch.\n#\n# These are synthetic, but they cannot be written as literals: the repository secret\n# scanner reads source lines, so a fixture like the vendor prefix followed by 16\n# characters is flagged here as if it were a live key. That is the same\n# false-positive class these tests exist to prevent, so each probe is split across a\n# concatenation boundary — no single literal below matches a secret pattern — and\n# padded with a zero-entropy filler.\n_PAD = \"0\" * 8 + \"a\" * 8 # 16 chars, no entropy\n\n\ndef _probe(*fragments: str) -> str:\n \"\"\"Join innocuous fragments into one credential-shaped string at runtime.\"\"\"\n return \"\".join(fragments) + _PAD\n\n\nSYNTHETIC_LEAKS = [\n _probe(\"Authorization: \", \"Bearer \", \"hf\", \"_\"),\n _probe(\"export \", \"HF\", \"_TOKEN=\", \"hf\", \"_\"),\n _probe(\"NGC\", \"_API\", \"_KEY=\", \"nvapi\", \"-\"),\n _probe(\"openai key \", \"sk\", \"-\"),\n _probe(\"pass\", \"word: \"),\n]\n\nCREDENTIAL_ASSERTION_MARKER = \"credential\"\n\n\ndef _load():\n return json.loads(EVALS_PATH.read_text())\n\n\nclass TestEvalsSchema(unittest.TestCase):\n @classmethod\n def setUpClass(cls):\n cls.data = _load()\n cls.cases = cls.data[\"evals\"]\n\n def test_skill_name_matches_directory(self):\n self.assertEqual(self.data[\"skill_name\"], SKILL_ROOT.name)\n\n def test_ids_are_unique_non_empty_strings(self):\n ids = [c[\"id\"] for c in self.cases]\n for case_id in ids:\n self.assertIsInstance(case_id, str, \"eval ids must be strings\")\n self.assertTrue(case_id.strip())\n self.assertEqual(len(ids), len(set(ids)), \"duplicate eval ids\")\n\n def test_every_case_has_the_required_fields(self):\n for case in self.cases:\n with self.subTest(case=case[\"id\"]):\n for field in (\"name\", \"prompt\", \"expected_output\", \"assertions\"):\n self.assertIn(field, case)\n self.assertTrue(case[\"assertions\"], \"case has no assertions\")\n\n def test_assertion_types_are_known(self):\n for case in self.cases:\n for assertion in case[\"assertions\"]:\n with self.subTest(case=case[\"id\"], text=assertion[\"text\"]):\n self.assertIn(assertion[\"type\"], VALID_ASSERTION_TYPES)\n\n def test_every_regex_compiles(self):\n for case in self.cases:\n for assertion in case[\"assertions\"]:\n if not assertion[\"type\"].endswith(\"_pattern\"):\n continue\n with self.subTest(case=case[\"id\"], text=assertion[\"text\"]):\n re.compile(assertion[\"pattern\"])\n\n\nclass TestBehaviorGrading(unittest.TestCase):\n \"\"\"Litmus grades activation from the transcript, not from prose.\"\"\"\n\n @classmethod\n def setUpClass(cls):\n cls.cases = _load()[\"evals\"]\n\n def test_every_case_declares_should_trigger(self):\n for case in self.cases:\n with self.subTest(case=case[\"id\"]):\n self.assertIn(\"should_trigger\", case)\n self.assertIsInstance(case[\"should_trigger\"], bool)\n\n def test_every_case_has_expected_behavior(self):\n for case in self.cases:\n with self.subTest(case=case[\"id\"]):\n behaviors = case.get(\"expected_behavior\")\n self.assertTrue(behaviors, \"expected_behavior is missing or empty\")\n for behavior in behaviors:\n self.assertIsInstance(behavior, str)\n self.assertTrue(behavior.strip())\n\n def test_suite_contains_negative_cases(self):\n negatives = [c for c in self.cases if c[\"should_trigger\"] is False]\n self.assertGreaterEqual(len(negatives), 3, \"too few anti-trigger cases\")\n\n def test_most_prompts_do_not_name_the_skill(self):\n \"\"\"Discoverability must be measured, not assumed (PROMPT_NAMES_SKILL).\"\"\"\n named = [c for c in self.cases if SKILL_ROOT.name in c[\"prompt\"]]\n self.assertLess(\n len(named),\n len(self.cases) / 2,\n \"over half the prompts name the skill outright, so discoverability is untested\",\n )\n\n\nclass TestNoNegationBlindGuards(unittest.TestCase):\n \"\"\"The regression guard for the defect class Litmus reported.\n\n Each ``not_contains_pattern`` is run against sentences a correct run emits. A\n match means the assertion would fail the *right* answer.\n \"\"\"\n\n @classmethod\n def setUpClass(cls):\n cls.cases = _load()[\"evals\"]\n\n def test_no_negative_guard_fires_on_a_correct_response(self):\n for case in self.cases:\n for assertion in case[\"assertions\"]:\n if assertion[\"type\"] != \"not_contains_pattern\":\n continue\n if CREDENTIAL_ASSERTION_MARKER in assertion[\"text\"].lower():\n continue\n pattern = re.compile(assertion[\"pattern\"], re.IGNORECASE)\n for sentence in ANTI_NEGATION_CORPUS:\n with self.subTest(case=case[\"id\"], text=assertion[\"text\"]):\n match = pattern.search(sentence)\n if match is not None:\n self.fail(\n \"negation-blind guard: matched \"\n f\"{match.group(0)!r} in a correct response: {sentence!r}\"\n )\n\n def test_negative_guards_are_anchored_to_an_invocation_shape(self):\n \"\"\"A bare identifier is what makes a guard negation-blind.\"\"\"\n for case in self.cases:\n for assertion in case[\"assertions\"]:\n if assertion[\"type\"] != \"not_contains_pattern\":\n continue\n if CREDENTIAL_ASSERTION_MARKER in assertion[\"text\"].lower():\n continue\n pattern = assertion[\"pattern\"]\n with self.subTest(case=case[\"id\"], text=assertion[\"text\"]):\n self.assertTrue(\n any(anchor in pattern for anchor in (\"(^|\\\\n)\", \"\\\\?\", \"\\\\s\")),\n \"guard must be anchored to a line start, a question, or a \"\n \"command shape rather than matching a bare identifier\",\n )\n\n\nclass TestCredentialGuard(unittest.TestCase):\n @classmethod\n def setUpClass(cls):\n cls.cases = _load()[\"evals\"]\n cls.patterns = [\n a[\"pattern\"]\n for c in cls.cases\n for a in c[\"assertions\"]\n if a[\"type\"] == \"not_contains_pattern\"\n and CREDENTIAL_ASSERTION_MARKER in a[\"text\"].lower()\n ]\n\n def test_the_suite_has_credential_guards(self):\n self.assertTrue(self.patterns)\n\n def test_all_credential_guards_are_identical(self):\n self.assertEqual(\n len(set(self.patterns)),\n 1,\n \"credential guards have drifted apart; keep one shared pattern\",\n )\n\n def test_credential_guard_does_not_fire_on_benign_strings(self):\n pattern = re.compile(self.patterns[0], re.IGNORECASE)\n for benign in BENIGN_STRINGS:\n with self.subTest(benign=benign):\n match = pattern.search(benign)\n if match is not None:\n self.fail(\n f\"false positive: matched {match.group(0)!r} in {benign!r}\"\n )\n\n def test_credential_guard_still_catches_real_leaks(self):\n pattern = re.compile(self.patterns[0], re.IGNORECASE)\n for leak in SYNTHETIC_LEAKS:\n with self.subTest(leak=leak):\n self.assertIsNotNone(\n pattern.search(leak),\n f\"credential guard missed a leak: {leak!r}\",\n )\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10712,"content_sha256":"a8dac30ff42b04996a4136d6a99a40905bc254f703d3eb91cf6b303de4107922"},{"filename":"tests/test_hardened_scripts.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nValidation tests for the hardened scripts in deepstream-import-vision-model.\n\nCovers:\n - install.sh: --target validation (rejects /, .., empty, missing)\n dry-run previews self-contained skill copies\n - scripts/model/cleanup.sh: MODEL_NAME regex enforcement, empty arg rejection,\n dry-run does not touch the filesystem\n - scripts/model/hf-list-files.sh / hf-download-config.sh:\n rejects injection characters in HF_ORG / MODEL_NAME\n - scripts/model/ngc-list-files.sh / ngc-download.sh:\n rejects injection characters in NGC args\n refuses invalid DEST_DIR (empty, /, containing ..)\n - scripts/deepstream/ds-kitti-dump.sh:\n usage message printed when args missing\n - scripts/report/md-to-html-pdf.py::embed_images:\n base64-inlines local images, rejects path traversal,\n leaves absolute/remote URLs alone\n\nRun:\n python3 -m unittest discover -s tests -v\n\"\"\"\nfrom __future__ import annotations\n\nimport base64\nimport os\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nSKILL_DIR = Path(__file__).resolve().parent.parent\nSCRIPTS = SKILL_DIR / \"scripts\"\nINSTALL_SH = SKILL_DIR / \"install.sh\"\n\n\ndef run_script(cmd, cwd=None, timeout=30):\n \"\"\"Run a command and return (returncode, stdout, stderr).\"\"\"\n r = subprocess.run(\n cmd,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n shell=False,\n cwd=str(cwd) if cwd else None,\n timeout=timeout,\n )\n return r.returncode, r.stdout, r.stderr\n\n\nclass TestInstallScript(unittest.TestCase):\n \"\"\"install.sh TARGET validation (B6).\"\"\"\n\n def test_rejects_missing_target_arg(self):\n rc, out, err = run_script([\"bash\", str(INSTALL_SH)])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"--target is required\", out + err)\n\n def test_rejects_filesystem_root(self):\n rc, out, err = run_script([\"bash\", str(INSTALL_SH), \"--target\", \"/\", \"--dry-run\"])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid --target\", out + err)\n\n def test_rejects_path_traversal(self):\n rc, out, err = run_script(\n [\"bash\", str(INSTALL_SH), \"--target\", \"/tmp/../etc\", \"--dry-run\"]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid --target\", out + err)\n\n def test_rejects_nonexistent_target(self):\n rc, out, err = run_script(\n [\"bash\", str(INSTALL_SH), \"--target\", \"/does/not/exist\", \"--dry-run\"]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"target directory not found\", out + err)\n\n def test_dry_run_previews_all_skill_locations(self):\n with tempfile.TemporaryDirectory() as td:\n rc, out, _ = run_script(\n [\"bash\", str(INSTALL_SH), \"--target\", td, \"--dry-run\"]\n )\n self.assertEqual(rc, 0, f\"install.sh --dry-run failed: {out}\")\n self.assertIn(\"[dry-run] cp -r\", out)\n # Single skill copied as real files (references pattern — no sub-skills)\n self.assertIn(\".claude/skills/deepstream-import-vision-model\", out)\n self.assertNotIn(\"nv-model-acquire\", out)\n self.assertNotIn(\"nv-engine-build\", out)\n self.assertNotIn(\"ds-run-pipeline\", out)\n self.assertNotIn(\"nv-import-vision-model-report\", out)\n # Cursor skill also installed\n self.assertIn(\".cursor/skills/deepstream-import-vision-model\", out)\n self.assertIn(\".codex/skills/deepstream-import-vision-model\", out)\n # No agent directory — skill-only architecture\n self.assertNotIn(\".claude/agents\", out)\n # Nothing must be written in dry-run mode\n self.assertEqual(os.listdir(td), [])\n\n\n def test_reinstall_from_installed_path_is_safe(self):\n with tempfile.TemporaryDirectory() as td:\n installed = Path(td) / \".claude\" / \"skills\" / \"deepstream-import-vision-model\"\n shutil.copytree(SKILL_DIR, installed)\n marker = installed / \"SKILL.md\"\n rc, out, err = run_script([\n \"bash\", str(installed / \"install.sh\"),\n \"--target\", td, \"--no-cursor\",\n ])\n self.assertEqual(rc, 0, f\"stdout={out} stderr={err}\")\n self.assertTrue(marker.exists(), \"in-place reinstall must not delete its source\")\n self.assertIn(\"source and destination are identical\", out)\n\n\nclass TestCleanupScript(unittest.TestCase):\n \"\"\"scripts/model/cleanup.sh MODEL_NAME validation (B8).\"\"\"\n\n CLEANUP = SCRIPTS / \"model\" / \"cleanup.sh\"\n\n def test_rejects_missing_arg(self):\n rc, out, err = run_script([\"bash\", str(self.CLEANUP)])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"Usage\", out + err)\n\n def test_rejects_shell_metachars(self):\n rc, _, err = run_script([\"bash\", str(self.CLEANUP), \"bad;name\", \"--dry-run\"])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"MODEL_NAME must match\", err)\n\n def test_rejects_slash(self):\n rc, _, err = run_script([\"bash\", str(self.CLEANUP), \"bad/name\", \"--dry-run\"])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"MODEL_NAME must match\", err)\n\n def test_accepts_valid_name_dry_run(self):\n with tempfile.TemporaryDirectory() as td:\n rc, out, err = run_script(\n [\"bash\", str(self.CLEANUP), \"yolov8n\", \"--dry-run\"], cwd=td\n )\n self.assertEqual(rc, 0, f\"stdout={out} stderr={err}\")\n # No candidates exist in a fresh dir — all should be skipped, nothing removed\n self.assertIn(\"skip (not present)\", out)\n\n def test_dry_run_does_not_remove_present_paths(self):\n with tempfile.TemporaryDirectory() as td:\n tdp = Path(td)\n target = tdp / \"build\" / \".venv_yolov8n\"\n target.mkdir(parents=True)\n (target / \"marker\").write_text(\"present\")\n rc, out, _ = run_script(\n [\"bash\", str(self.CLEANUP), \"yolov8n\", \"--dry-run\"], cwd=td\n )\n self.assertEqual(rc, 0)\n self.assertIn(\"[dry-run]\", out)\n self.assertTrue(target.exists(), \"dry-run must not remove files\")\n self.assertTrue((target / \"marker\").exists())\n\n\nclass TestHFScripts(unittest.TestCase):\n \"\"\"HF helper script input validation (B4).\"\"\"\n\n LIST = SCRIPTS / \"model\" / \"hf-list-files.sh\"\n CONFIG = SCRIPTS / \"model\" / \"hf-download-config.sh\"\n\n def test_list_rejects_bad_org(self):\n rc, _, err = run_script([\"bash\", str(self.LIST), \"bad;org\", \"model\"])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid characters\", err)\n\n def test_list_rejects_bad_model(self):\n rc, _, err = run_script([\"bash\", str(self.LIST), \"org\", \"bad$model\"])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid characters\", err)\n\n def test_list_rejects_missing_args(self):\n rc, _, err = run_script([\"bash\", str(self.LIST)])\n self.assertNotEqual(rc, 0)\n self.assertIn(\"Usage\", err)\n\n def test_config_rejects_path_traversal_in_dest(self):\n rc, _, err = run_script(\n [\n \"bash\",\n str(self.CONFIG),\n \"org\",\n \"model\",\n \"relative/../etc/config.json\",\n ]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"'..'\", err)\n\n def test_config_rejects_absolute_dest(self):\n rc, _, err = run_script(\n [\n \"bash\",\n str(self.CONFIG),\n \"org\",\n \"model\",\n \"/tmp/config.json\",\n ]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"must be relative\", err)\n\n\nclass TestNGCScripts(unittest.TestCase):\n \"\"\"NGC helper script input validation (B5).\"\"\"\n\n LIST = SCRIPTS / \"model\" / \"ngc-list-files.sh\"\n DOWNLOAD = SCRIPTS / \"model\" / \"ngc-download.sh\"\n\n def test_list_rejects_bad_org(self):\n rc, _, err = run_script(\n [\"bash\", str(self.LIST), \"bad;org\", \"team\", \"model\", \"v1\"]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid characters\", err)\n\n def test_download_rejects_empty_dest(self):\n rc, _, err = run_script(\n [\n \"bash\",\n str(self.DOWNLOAD),\n \"org\",\n \"team\",\n \"model\",\n \"v1\",\n \"\",\n ]\n )\n self.assertNotEqual(rc, 0)\n\n def test_download_rejects_traversal_dest(self):\n rc, _, err = run_script(\n [\n \"bash\",\n str(self.DOWNLOAD),\n \"org\",\n \"team\",\n \"model\",\n \"v1\",\n \"/tmp/../etc\",\n ]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid DEST_DIR\", err)\n\n def test_download_rejects_fs_root_dest(self):\n rc, _, err = run_script(\n [\n \"bash\",\n str(self.DOWNLOAD),\n \"org\",\n \"team\",\n \"model\",\n \"v1\",\n \"/\",\n ]\n )\n self.assertNotEqual(rc, 0)\n self.assertIn(\"invalid DEST_DIR\", err)\n\n\nclass TestKittiDumpUsage(unittest.TestCase):\n \"\"\"scripts/deepstream/ds-kitti-dump.sh usage message (B7).\"\"\"\n\n KITTI = SCRIPTS / \"deepstream\" / \"ds-kitti-dump.sh\"\n\n def test_prints_usage_when_args_missing(self):\n rc, out, err = run_script([\"bash\", str(self.KITTI)])\n self.assertNotEqual(rc, 0)\n combined = out + err\n self.assertTrue(\n \"Usage\" in combined or \"unbound variable\" in combined,\n f\"expected Usage or unbound-variable error, got: {combined!r}\",\n )\n\n\nclass TestEmbedImages(unittest.TestCase):\n \"\"\"scripts/report/md-to-html-pdf.py::embed_images (B2).\n\n Images must be base64-inlined so wkhtmltopdf no longer needs\n --enable-local-file-access.\n \"\"\"\n\n @classmethod\n def setUpClass(cls):\n # Import the module by loading its source directly to avoid hyphen-path issues.\n # md-to-html-pdf.py imports the third-party `markdown` package at module scope,\n # which isn't needed for embed_images(). Stub it so tests run without that dep.\n import importlib.util\n import types\n\n if \"markdown\" not in sys.modules:\n stub = types.ModuleType(\"markdown\")\n stub.markdown = lambda text, **kw: text # no-op for tests\n sys.modules[\"markdown\"] = stub\n\n src = SCRIPTS / \"report\" / \"md-to-html-pdf.py\"\n spec = importlib.util.spec_from_file_location(\"md_to_html_pdf\", src)\n cls.mod = importlib.util.module_from_spec(spec)\n spec.loader.exec_module(cls.mod)\n\n def test_inlines_local_png(self):\n # 1x1 transparent PNG\n png = base64.b64decode(\n b\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=\"\n )\n with tempfile.TemporaryDirectory() as td:\n p = Path(td)\n (p / \"pic.png\").write_bytes(png)\n html = '\u003cimg src=\"pic.png\" alt=\"x\">'\n out = self.mod.embed_images(html, str(p))\n self.assertIn(\"data:image/png;base64,\", out)\n self.assertNotIn('src=\"pic.png\"', out)\n\n def test_leaves_remote_url_alone(self):\n html = '\u003cimg src=\"https://example.com/pic.png\" alt=\"x\">'\n with tempfile.TemporaryDirectory() as td:\n out = self.mod.embed_images(html, td)\n self.assertEqual(html, out)\n\n def test_leaves_absolute_path_alone(self):\n html = '\u003cimg src=\"/opt/data/elsewhere.png\" alt=\"x\">'\n with tempfile.TemporaryDirectory() as td:\n out = self.mod.embed_images(html, td)\n # Should not be rewritten to a data: URI (absolute paths are skipped)\n self.assertNotIn(\"data:\", out)\n self.assertIn('src=\"/opt/data/elsewhere.png\"', out)\n\n def test_rejects_path_traversal(self):\n # src points outside base_dir — must not be embedded\n with tempfile.TemporaryDirectory() as td_base:\n with tempfile.TemporaryDirectory() as td_outside:\n secret = Path(td_outside) / \"secret.png\"\n secret.write_bytes(b\"\\x89PNG\\r\\n\\x1a\\nFAKE\")\n rel = os.path.relpath(secret, td_base) # starts with ../\n html = f'\u003cimg src=\"{rel}\" alt=\"x\">'\n out = self.mod.embed_images(html, td_base)\n self.assertNotIn(\"data:\", out)\n self.assertIn(rel, out)\n\n\nif __name__ == \"__main__\":\n unittest.main(verbosity=2)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13623,"content_sha256":"5e8093db58831f02dcdf1eabf128772f35e2605c2f441386d92e10d0c14c3bd6"},{"filename":"tests/test_onnx_export_contract.py","content":"# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\n\"\"\"Contract tests for the SafeTensors -> ONNX export path.\n\nThese run without torch, a GPU, or any model download: they check the wrapper script's\nCLI contract and the exporter's static behaviour (input-size resolution, architecture\ngating), which is what actually breaks when the export path is refactored.\n\nThe real export is exercised in-container against the DeepStream image; see\nreferences/model-acquire.md step 2b-iii.\n\nRun:\n python3 -m unittest discover -s tests -v\n\"\"\"\nfrom __future__ import annotations\n\nimport ast\nimport json\nimport subprocess\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nSKILL_DIR = Path(__file__).resolve().parent.parent\nEXPORT_SH = SKILL_DIR / \"scripts\" / \"model\" / \"safetensors-to-onnx.sh\"\nEXPORT_PY = SKILL_DIR / \"scripts\" / \"model\" / \"safetensors_to_onnx.py\"\nREQUIREMENTS = SKILL_DIR / \"scripts\" / \"requirements.txt\"\n\n\ndef _load_exporter_module():\n \"\"\"Import safetensors_to_onnx with torch/transformers stubbed.\n\n Those live only in the container venv. Stubs are installed into sys.modules just long\n enough for the import, then removed, so nothing leaks into other tests.\n \"\"\"\n import importlib\n import sys\n import types\n\n torch_stub = types.ModuleType(\"torch\")\n nn_stub = types.ModuleType(\"torch.nn\")\n\n class _Module: # stand-in base class for DetectionWrapper\n def __init__(self, *args, **kwargs):\n pass\n\n nn_stub.Module = _Module\n torch_stub.nn = nn_stub\n torch_stub.Tensor = object\n\n tf_stub = types.ModuleType(\"transformers\")\n tf_stub.AutoConfig = object\n tf_stub.AutoModelForObjectDetection = object\n\n injected = {\"torch\": torch_stub, \"torch.nn\": nn_stub, \"transformers\": tf_stub}\n saved = {name: sys.modules.get(name) for name in injected}\n sys.modules.update(injected)\n module_dir = str(EXPORT_PY.parent)\n sys.path.insert(0, module_dir)\n try:\n sys.modules.pop(\"safetensors_to_onnx\", None)\n return importlib.import_module(\"safetensors_to_onnx\")\n finally:\n sys.path.remove(module_dir)\n sys.modules.pop(\"safetensors_to_onnx\", None)\n for name, previous in saved.items():\n if previous is None:\n sys.modules.pop(name, None)\n else:\n sys.modules[name] = previous\n\n\nclass TestExportWrapperScript(unittest.TestCase):\n def test_wrapper_exists_and_parses(self):\n self.assertTrue(EXPORT_SH.is_file(), f\"missing {EXPORT_SH}\")\n rc = subprocess.run([\"bash\", \"-n\", str(EXPORT_SH)], capture_output=True, text=True)\n self.assertEqual(rc.returncode, 0, rc.stderr)\n\n def test_wrapper_rejects_missing_args(self):\n rc = subprocess.run([\"bash\", str(EXPORT_SH)], capture_output=True, text=True)\n self.assertNotEqual(rc.returncode, 0)\n self.assertIn(\"Usage:\", rc.stdout + rc.stderr)\n\n def test_wrapper_errors_without_venv(self):\n \"\"\"Without the container-built venv it must fail with bootstrap guidance, not a traceback.\"\"\"\n with tempfile.TemporaryDirectory() as td:\n rc = subprocess.run(\n [\"bash\", str(EXPORT_SH), \"some/model\", f\"{td}/out\"],\n capture_output=True, text=True,\n )\n combined = rc.stdout + rc.stderr\n # Either the venv is genuinely absent (expected on a dev box) or it exists and the\n # export proceeds; only the absent case is asserted here.\n if rc.returncode != 0:\n self.assertIn(\"venv not found\", combined)\n self.assertIn(\"setup.sh\", combined)\n\n def test_wrapper_no_longer_invokes_optimum(self):\n text = EXPORT_SH.read_text()\n self.assertNotIn(\"optimum-cli export\", text)\n self.assertIn(\"safetensors_to_onnx.py\", text)\n\n\nclass TestExporterModule(unittest.TestCase):\n \"\"\"Static checks on the exporter — no torch import required.\"\"\"\n\n @classmethod\n def setUpClass(cls):\n cls.source = EXPORT_PY.read_text()\n cls.tree = ast.parse(cls.source)\n\n def test_module_parses(self):\n self.assertTrue(EXPORT_PY.is_file(), f\"missing {EXPORT_PY}\")\n\n def test_offers_both_export_backends(self):\n \"\"\"Neither backend handles every architecture, so both must be present.\n\n dynamo copes with models the tracer chokes on; TorchScript is the one that keeps\n RT-DETR's batch dimension dynamic (dynamo specializes it to the trace batch).\n \"\"\"\n fns = {n.name for n in ast.walk(self.tree) if isinstance(n, ast.FunctionDef)}\n self.assertIn(\"export_dynamo\", fns)\n self.assertIn(\"export_torchscript\", fns)\n self.assertIn(\"dynamo=True\", self.source)\n self.assertIn(\"dynamo=False\", self.source)\n\n def test_each_backend_uses_its_own_dynamic_shape_parameter(self):\n \"\"\"dynamic_shapes belongs to dynamo, dynamic_axes to TorchScript — mixing is a silent no-op.\"\"\"\n self.assertIn(\"torch.export.Dim\", self.source)\n self.assertIn(\"dynamic_shapes\", self.source)\n self.assertIn(\"dynamic_axes\", self.source)\n\n def test_falls_back_when_a_backend_specializes_batch(self):\n \"\"\"A static batch dim must trigger the next backend, not be accepted.\"\"\"\n self.assertIn(\"export_torchscript\", self.source.split(\"strategies\")[1])\n self.assertIn(\"static batch dimension\", self.source)\n\n def test_opset_defaults_to_18(self):\n \"\"\"Opset 17 fails the dynamo downgrade pass: 'No Adapter To Version 17 for Resize'.\"\"\"\n self.assertRegex(self.source, r'\"--opset\".*?default=18')\n\n def test_declares_deepstream_output_contract(self):\n self.assertIn('\"pixel_values\"', self.source)\n self.assertIn('\"logits\"', self.source)\n self.assertIn('\"pred_boxes\"', self.source)\n\n def test_writes_model_onnx_filename(self):\n \"\"\"Downstream steps copy models/\u003cname>/onnx_export/model.onnx — keep that name.\"\"\"\n self.assertIn('\"model.onnx\"', self.source)\n\n def test_verifies_dynamic_batch_rather_than_assuming(self):\n \"\"\"A backend can silently bake in a static batch dim; it must be checked, not assumed.\"\"\"\n fns = {n.name for n in ast.walk(self.tree) if isinstance(n, ast.FunctionDef)}\n self.assertIn(\"verify\", fns)\n self.assertIn(\"consolidate_external_data\", fns)\n # verify() reports whether the batch axis survived so the caller can switch backends.\n self.assertIn(\"dim_param\", self.source)\n\n def test_rejects_non_detection_architectures(self):\n fns = {n.name for n in ast.walk(self.tree) if isinstance(n, ast.FunctionDef)}\n self.assertIn(\"assert_detection_architecture\", fns)\n self.assertIn(\"ForObjectDetection\", self.source)\n\n def test_input_size_resolution_handles_known_preprocessor_shapes(self):\n \"\"\"Exercise resolve_input_size for real, importing the module with torch stubbed.\n\n The function is pure stdlib, but the module imports torch/transformers at top level,\n which are container-only. Stubbing sys.modules keeps this runnable on a dev box\n without resorting to dynamic code evaluation, which Tier-1 flags as AST1/AST8.\n \"\"\"\n resolve = _load_exporter_module().resolve_input_size\n\n with tempfile.TemporaryDirectory() as td:\n d = Path(td)\n self.assertEqual(resolve(str(d), 512), (512, 512), \"explicit override wins\")\n self.assertEqual(resolve(str(d), None), (640, 640), \"no config -> 640 default\")\n\n (d / \"preprocessor_config.json\").write_text(\n json.dumps({\"size\": {\"height\": 800, \"width\": 1333}}))\n self.assertEqual(resolve(str(d), None), (800, 1333), \"height/width honoured\")\n\n (d / \"preprocessor_config.json\").write_text(\n json.dumps({\"size\": {\"shortest_edge\": 800, \"longest_edge\": 1333}}))\n self.assertEqual(resolve(str(d), None), (800, 800), \"shortest_edge -> square\")\n\n (d / \"preprocessor_config.json\").write_text(\"{ not valid json\")\n self.assertEqual(resolve(str(d), None), (640, 640), \"malformed config falls back\")\n\n\nclass TestExportDependencies(unittest.TestCase):\n def test_optimum_is_gone(self):\n self.assertNotIn(\"optimum[exporters]\", REQUIREMENTS.read_text())\n\n def test_onnxscript_pinned_for_dynamo(self):\n \"\"\"The dynamo ONNX backend needs onnxscript at runtime.\"\"\"\n self.assertRegex(REQUIREMENTS.read_text(), r\"(?m)^onnxscript==\")\n\n def test_transformers_past_the_rce_fixes(self):\n \"\"\"GHSA-29pf-2h5f-8g72 is fixed in 5.3.0 and GHSA-fgcw-684q-jj6r in 5.5.0.\"\"\"\n import re\n m = re.search(r\"(?m)^transformers==(\\d+)\\.(\\d+)\\.(\\d+)\", REQUIREMENTS.read_text())\n self.assertIsNotNone(m, \"transformers must be pinned\")\n major, minor = int(m.group(1)), int(m.group(2))\n self.assertGreaterEqual((major, minor), (5, 5),\n \"transformers must be >= 5.5.0 to clear both HIGH RCE advisories\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9120,"content_sha256":"7f3fcd8e164c2b0a755161155e06b5d018f72271d87362717619aeb6536b648d"},{"filename":"tests/test_powershell_installer.py","content":"#!/usr/bin/env python3\n\n# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Regression tests for the Windows PowerShell installers of BOTH DeepStream skills.\n\nWhy this exists\n---------------\n`install.ps1` once shipped with:\n\n Copy-Item -LiteralPath $SkillDir -Destination $dest -Recurse -Force\n\nwhich is not portable. Whether it copies a folder's CONTENTS or the folder ITSELF depends on\nwhether the destination already exists, and on Windows PowerShell 5.1 it created the directory\ntree without copying any leaf files -- installing an empty skill directory with no SKILL.md, so\nthe agent runtime silently refused to load the skill. Nothing caught it, because the test suites\nonly ever exercised install.sh.\n\nThe central assertion here is therefore a strict **file-count match** against the source, not just\n\"SKILL.md exists\": a partial copy is exactly the failure mode that shipped.\n\nRunner\n------\nNeeds a PowerShell. Discovery order: `pwsh`, `powershell`, then the `mcr.microsoft.com/powershell`\ncontainer if docker has it locally (so this runs on Linux dev boxes and CI too). Skips cleanly when\nnone is available -- never a false failure on a machine without PowerShell.\n\nCaveat: the container/dev-box runner is PowerShell 7. The bug that motivated this was 5.1-specific,\nso this pins the copy *contract* rather than reproducing every 5.1 quirk. Run it on Windows for\nfull fidelity.\n\nRun:\n python3 -m unittest discover -s tests -v\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimport subprocess\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nREPO_SKILLS = Path(__file__).resolve().parents[2]\nPS_IMAGE = \"mcr.microsoft.com/powershell:latest\"\n\n# (skill dir name, expected file count key) -- both installers share the copy logic under test.\nSKILLS = [\"deepstream-import-vision-model\", \"deepstream-eval-and-finetune\"]\n\n# Machine-local build artifacts each installer strips from the copy; excluded when computing the\n# expected file count. Mirrors the strip rules inside the two install.ps1 scripts.\nSTRIP_SUFFIXES = (\".pyc\", \".so\", \".o\")\nSTRIP_NAMES = (\"ds_image_eval\",)\n\n\ndef _docker_has_ps_image() -> bool:\n if not shutil.which(\"docker\"):\n return False\n try:\n out = subprocess.run([\"docker\", \"images\", \"-q\", PS_IMAGE],\n capture_output=True, text=True, timeout=30)\n return bool(out.stdout.strip())\n except Exception:\n return False\n\n\ndef _runner():\n \"\"\"Return a callable(script, args, cwd) -> CompletedProcess, or None if no PowerShell.\"\"\"\n for exe in (\"pwsh\", \"powershell\"):\n if shutil.which(exe):\n def run_native(script, args, cwd, _exe=exe):\n return subprocess.run(\n [_exe, \"-NoProfile\", \"-NonInteractive\", \"-File\", str(script), *args],\n cwd=str(cwd), capture_output=True, text=True, timeout=300)\n return run_native\n if _docker_has_ps_image():\n def run_docker(script, args, cwd):\n # Mount both trees at their real absolute paths so in-container paths match the host's\n # and the assertions below can read results back directly. `-File` (not `-Command`) so\n # that -Target/-NoCursor bind as named parameters rather than positional strings.\n return subprocess.run(\n [\"docker\", \"run\", \"--rm\",\n # Write as the invoking user, or the installed tree lands root-owned and the\n # caller cannot clean up its own temp dir.\n \"--user\", f\"{os.getuid()}:{os.getgid()}\",\n \"-e\", \"HOME=/tmp\",\n \"-v\", f\"{REPO_SKILLS}:{REPO_SKILLS}:ro\",\n \"-v\", f\"{cwd}:{cwd}\",\n \"-w\", str(cwd), PS_IMAGE,\n \"pwsh\", \"-NoProfile\", \"-NonInteractive\",\n \"-File\", str(script), *args],\n capture_output=True, text=True, timeout=600)\n return run_docker\n return None\n\n\nRUN = _runner()\n\n\ndef _expected_files(src: Path) -> set[str]:\n \"\"\"Files the installer should land, relative to the skill root, after its strip rules.\"\"\"\n out = set()\n for p in src.rglob(\"*\"):\n if not p.is_file():\n continue\n if \"__pycache__\" in p.parts:\n continue\n if p.suffix in STRIP_SUFFIXES or p.name in STRIP_NAMES:\n continue\n out.add(p.relative_to(src).as_posix())\n return out\n\n\[email protected](RUN is None,\n \"no PowerShell available (need pwsh/powershell on PATH, or the \"\n f\"{PS_IMAGE} docker image pulled)\")\nclass TestPowerShellInstaller(unittest.TestCase):\n def _install(self, skill, target, extra=()):\n # Copy the skill out of the repo first: the installer resolves $PSScriptRoot, and the repo\n # checkout is mounted read-only under the docker runner.\n src = Path(target) / \"_src\" / skill\n src.parent.mkdir(parents=True, exist_ok=True)\n shutil.copytree(REPO_SKILLS / skill, src)\n proj = Path(target) / \"proj\"\n proj.mkdir()\n # --no-plugin equivalent: never touch the host's real ~/.claude during a test.\n args = [\"-Target\", str(proj), \"-NoCursor\", *extra]\n if skill == \"deepstream-eval-and-finetune\":\n args.append(\"-NoPlugin\")\n res = RUN(src / \"install.ps1\", args, target)\n return res, proj, src\n\n def test_installs_every_source_file_including_skill_md(self):\n for skill in SKILLS:\n with self.subTest(skill=skill), tempfile.TemporaryDirectory() as td:\n res, proj, src = self._install(skill, td)\n self.assertEqual(res.returncode, 0,\n f\"installer failed\\nstdout={res.stdout}\\nstderr={res.stderr}\")\n dest = proj / \".claude\" / \"skills\" / skill\n self.assertTrue((dest / \"SKILL.md\").is_file(),\n f\"SKILL.md missing -> the skill would not load. stdout={res.stdout}\")\n # The regression: an empty/partial tree used to pass silently.\n got = {p.relative_to(dest).as_posix() for p in dest.rglob(\"*\") if p.is_file()}\n self.assertEqual(got, _expected_files(src),\n \"installed file set does not match the source\")\n\n def test_installs_for_codex_as_well_as_claude(self):\n for skill in SKILLS:\n with self.subTest(skill=skill), tempfile.TemporaryDirectory() as td:\n _, proj, _ = self._install(skill, td)\n for agent in (\".claude\", \".codex\"):\n self.assertTrue((proj / agent / \"skills\" / skill / \"SKILL.md\").is_file(),\n f\"{agent} install is missing SKILL.md\")\n\n def test_no_cursor_flag_is_honoured(self):\n skill = SKILLS[0]\n with tempfile.TemporaryDirectory() as td:\n _, proj, _ = self._install(skill, td)\n self.assertFalse((proj / \".cursor\").exists(), \"-NoCursor still wrote .cursor/\")\n\n def test_reinstall_is_idempotent_and_does_not_nest(self):\n for skill in SKILLS:\n with self.subTest(skill=skill), tempfile.TemporaryDirectory() as td:\n _, proj, src = self._install(skill, td)\n dest = proj / \".claude\" / \"skills\" / skill\n first = {p.relative_to(dest).as_posix() for p in dest.rglob(\"*\") if p.is_file()}\n args = [\"-Target\", str(proj), \"-NoCursor\"]\n if skill == \"deepstream-eval-and-finetune\":\n args.append(\"-NoPlugin\")\n res = RUN(src / \"install.ps1\", args, td)\n self.assertEqual(res.returncode, 0, f\"reinstall failed: {res.stderr}\")\n again = {p.relative_to(dest).as_posix() for p in dest.rglob(\"*\") if p.is_file()}\n self.assertEqual(first, again, \"reinstall changed the installed file set\")\n self.assertFalse((dest / skill).exists(),\n \"reinstall nested the skill inside itself\")\n\n def test_install_fails_loudly_when_skill_md_would_be_missing(self):\n \"\"\"The guard must fire -- a silent partial install is the failure mode that shipped.\"\"\"\n skill = SKILLS[0]\n with tempfile.TemporaryDirectory() as td:\n src = Path(td) / \"_src\" / skill\n src.parent.mkdir(parents=True)\n shutil.copytree(REPO_SKILLS / skill, src)\n (src / \"SKILL.md\").unlink()\n proj = Path(td) / \"proj\"\n proj.mkdir()\n res = RUN(src / \"install.ps1\", [\"-Target\", str(proj), \"-NoCursor\"], td)\n self.assertNotEqual(res.returncode, 0,\n \"installer reported success with SKILL.md absent\")\n self.assertIn(\"SKILL.md\", res.stdout + res.stderr)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9454,"content_sha256":"d8c745ebcbc40f45ded039e9a7448e3e318410a67e44ce0c86bbf52ba46384d6"},{"filename":"tests/test_skill_model_intake.py","content":"# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n# SPDX-License-Identifier: Apache-2.0\n\nimport re\nimport unittest\nfrom pathlib import Path\n\n\nSKILL_ROOT = Path(__file__).resolve().parents[1]\n\n\nclass TestSkillModelIntake(unittest.TestCase):\n @classmethod\n def setUpClass(cls):\n cls.skill = (SKILL_ROOT / \"SKILL.md\").read_text()\n cls.acquire = (SKILL_ROOT / \"references\" / \"model-acquire.md\").read_text()\n cls.choice = cls.skill.split(\n \"## Model choice — always offer two options\", 1\n )[1].split(\"## Pipeline Overview\", 1)[0]\n\n def test_exactly_two_ordered_model_choices_are_offered(self):\n self.assertEqual(\n re.findall(r\"^### ([12])\\. \", self.choice, flags=re.MULTILINE),\n [\"1\", \"2\"],\n )\n\n def test_default_is_the_validated_rtdetr_model(self):\n self.assertIn(\"Default model (recommended)\", self.choice)\n self.assertIn(\"PekingU/rtdetr_r50vd\", self.choice)\n\n def test_custom_choice_accepts_hf_or_versioned_ngc(self):\n self.assertIn(\"Hugging Face model ID\", self.choice)\n self.assertIn(\"NVIDIA NGC catalog model URL including its version\", self.choice)\n self.assertIn(\"rejects classification, segmentation\", self.choice)\n\n def test_model_acquire_runbook_repeats_the_choice_gate(self):\n intake = self.acquire.split(\"## Intake — choose the model\", 1)[1].split(\n \"## MANDATORY\", 1\n )[0]\n self.assertIn(\"Default model (recommended)\", intake)\n self.assertIn(\"Custom object-detection model\", intake)\n self.assertIn('INPUT=\"PekingU/rtdetr_r50vd\"', intake)\n\n def test_dry_run_has_no_side_effects(self):\n normalized = \" \".join(self.choice.split())\n for phrase in (\n \"without browsing\",\n \"downloading\",\n \"launching Docker\",\n \"writing files\",\n \"starting processes\",\n ):\n self.assertIn(phrase, normalized)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2058,"content_sha256":"d797f67fca2ea63424ef4508042e61e0b836d2f477d055c85ee4107e5664f8ea"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"DeepStream Import Vision Model","type":"text"}]},{"type":"paragraph","content":[{"text":"When this skill is active, ","type":"text"},{"text":"read the relevant reference document before starting each phase","type":"text","marks":[{"type":"strong"}]},{"text":". Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules.","type":"text"}]},{"type":"paragraph","content":[{"text":"Current scope:","type":"text","marks":[{"type":"strong"}]},{"text":" Object detection models only. Fail fast on classification, segmentation, or other architectures detected in ","type":"text"},{"text":"config.json","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Model choice — always offer two options","type":"text"}]},{"type":"paragraph","content":[{"text":"Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the user's request already clearly selects a model, confirm the matching choice instead of asking redundantly.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Default model (recommended)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the validated Hugging Face RT-DETR model:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"model_id: PekingU/rtdetr_r50vd\nsource: huggingface\ntask: object-detection\nprecision_preference: fp16","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Custom object-detection model","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask for one supported source:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hugging Face model ID (","type":"text"},{"text":"organization/model","type":"text","marks":[{"type":"code_inline"}]},{"text":") or full model URL.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NVIDIA NGC catalog model URL including its version.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Explain that the skill currently rejects classification, segmentation, and other non-detection architectures after inspecting ","type":"text"},{"text":"config.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do not invent or silently substitute a model when the custom source is missing or unsupported.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a dry run, present the same two choices and simulate discovery, build, benchmark, and report stages without browsing, downloading, launching Docker, writing files, or starting processes.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pipeline Overview","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reference","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it does","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Model Acquire","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/model-acquire.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/model-acquire.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Browse HF/NGC, detect format, download ONNX or export SafeTensors","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4–5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Engine Build","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/engine-build.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/engine-build.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Build dynamic TRT engine, run trtexec BS=1 and BS=MAX_BS","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6–7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DS Pipeline","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/pipeline-run.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/pipeline-run.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom bbox parser, nvinfer config, single-stream + multi-stream benchmarks","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Report","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/report-generation.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/report-generation.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5 charts, HTML, PDF benchmark report","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Run the full pipeline autonomously without pausing for confirmation at each step.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Runs entirely through Docker (no host packages)","type":"text"}]},{"type":"paragraph","content":[{"text":"Every step runs INSIDE the DeepStream container.","type":"text","marks":[{"type":"strong"}]},{"text":" The host needs only ","type":"text"},{"text":"Docker + the NVIDIA driver","type":"text","marks":[{"type":"strong"}]},{"text":" — no host python/venv/torch/trtexec/make/wkhtmltopdf. This works identically on Linux and ","type":"text"},{"text":"Windows","type":"text","marks":[{"type":"strong"}]},{"text":" (Docker Desktop + WSL2 backend, required for ","type":"text"},{"text":"--gpus","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The per-shell bind-mount token is the only OS difference — ","type":"text"},{"text":"-v \"$PWD\":/work","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bash), ","type":"text"},{"text":"-v \"${PWD}:/work\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" (PowerShell), ","type":"text"},{"text":"-v \"%cd%:/work\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" (cmd); full guide in ","type":"text"},{"text":"references/windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/windows.md","title":null}}]},{"text":". All venv/ONNX/ engine/parser/config/report artifacts live under the mounted working root and persist between the ephemeral ","type":"text"},{"text":"--rm","type":"text","marks":[{"type":"code_inline"}]},{"text":" containers.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pre-flight — bootstrap + verify (through the container)","type":"text"}]},{"type":"paragraph","content":[{"text":"1. One-time bootstrap","type":"text","marks":[{"type":"strong"}]},{"text":" — builds ","type":"text"},{"text":"build/.venv_optimum","type":"text","marks":[{"type":"code_inline"}]},{"text":" (torch/onnx/onnxruntime/report deps; the venv name is historical, optimum is no longer used) + installs ","type":"text"},{"text":"wkhtmltopdf","type":"text","marks":[{"type":"code_inline"}]},{"text":", all in-container. From the working root:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"docker run --rm -it --gpus all --shm-size=16g -v \"$PWD\":/work -w /work \\\n --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\\n .claude/skills/deepstream-import-vision-model/setup.sh","type":"text"}]},{"type":"paragraph","content":[{"text":"2. Preflight","type":"text","marks":[{"type":"strong"}]},{"text":" — GPU + venv + trtexec, run THROUGH the container (container-mode auto-detects):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"docker run --rm --gpus all -v \"$PWD\":/work -w /work \\\n --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\\n .claude/skills/deepstream-import-vision-model/scripts/preflight.sh # proceed only on PASS","type":"text"}]},{"type":"paragraph","content":[{"text":"Every subsequent phase runs the same way","type":"text","marks":[{"type":"strong"}]},{"text":" — issue the model's commands via ","type":"text"},{"text":"docker run … --entrypoint bash … -lc '\u003ccommands>'","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or the ","type":"text"},{"text":".claude/skills/deepstream-import-vision-model/scripts/dsrun.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" wrapper: ","type":"text"},{"text":"bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '\u003cin-container command>'","type":"text","marks":[{"type":"code_inline"}]},{"text":"), using ","type":"text"},{"text":"PY=build/.venv_optimum/bin/python","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"trtexec","type":"text","marks":[{"type":"code_inline"}]},{"text":" at ","type":"text"},{"text":"/usr/src/tensorrt/bin/trtexec","type":"text","marks":[{"type":"code_inline"}]},{"text":" inside the container. ","type":"text"},{"text":"deepstream-app","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"gst-launch-1.0","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"/opt/nvidia/deepstream/…","type":"text","marks":[{"type":"code_inline"}]},{"text":" sample paths all exist ","type":"text"},{"text":"in","type":"text","marks":[{"type":"strong"}]},{"text":" the image. TensorRT build+runtime share one image, so there is ","type":"text"},{"text":"no version skew","type":"text","marks":[{"type":"strong"}]},{"text":" (the concern the old \"build on the host\" rule tried to avoid — see ","type":"text"},{"text":"references/engine-build.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/engine-build.md","title":null}}]},{"text":"). ","type":"text"},{"text":"sample_720p.mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":" ships in the image; set ","type":"text"},{"text":"DS_VIDEO","type":"text","marks":[{"type":"code_inline"}]},{"text":" only to override.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Mandatory Output Structure","type":"text"}]},{"type":"paragraph","content":[{"text":"Create once ","type":"text"},{"text":"MODEL_NAME","type":"text","marks":[{"type":"code_inline"}]},{"text":" is known (Step 1). Never dump files flat.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"models/{model_name}/\n model/ \u003c- ONNX file(s)\n parser/ \u003c- .cpp, Makefile, .so\n config/ \u003c- nvinfer config, ds-app config, labels.txt\n scripts/ \u003c- run helper scripts\n benchmarks/\n engines/ \u003c- _dynamic_b{MAX_BS}.engine, timing.cache, build logs\n b1/ \u003c- trtexec BS=1 log\n b{MAX_BS}/ \u003c- trtexec BS=MAX_BS log\n ds/ \u003c- DS benchmark logs\n reports/ \u003c- benchmark_report.md, .html, .pdf, benchmark_data.json\n charts/ \u003c- chart_*.png (5 charts)\n samples/ \u003c- output .mp4 or .ogv (theoraenc fallback), test frames\n kitti_output/ \u003c- KITTI detection .txt files","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,benchmarks/ds,reports/charts,samples/kitti_output}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Critical Rules","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Engine naming","type":"text","marks":[{"type":"strong"}]},{"text":" — always ","type":"text"},{"text":"{model}_dynamic_b{MAX_BS}.engine","type":"text","marks":[{"type":"code_inline"}]},{"text":". Never bare ","type":"text"},{"text":"model_dynamic.engine","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"batch_size == num_streams","type":"text","marks":[{"type":"strong"}]},{"text":" — in DS runs, ","type":"text"},{"text":"batch-size","type":"text","marks":[{"type":"code_inline"}]},{"text":" and stream count are always equal.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Log filenames are fixed","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"trtexec_b1.log","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"trtexec_b${MAX_BS}.log","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ds_s${N}_run1.log","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ds_s${N}_run2.log","type":"text","marks":[{"type":"code_inline"}]},{"text":". No timestamps. Report generation reads exact paths.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parser zero-init","type":"text","marks":[{"type":"strong"}]},{"text":" — always ","type":"text"},{"text":"NvDsInferObjectDetectionInfo obj = {};","type":"text","marks":[{"type":"code_inline"}]},{"text":". Required for DS 9.1 OBB support; bare ","type":"text"},{"text":"obj;","type":"text","marks":[{"type":"code_inline"}]},{"text":" leaves ","type":"text"},{"text":"rotation_angle","type":"text","marks":[{"type":"code_inline"}]},{"text":" uninitialized, causing tilted bounding boxes.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"KITTI validation gate","type":"text","marks":[{"type":"strong"}]},{"text":" — do NOT proceed to Step 7 if KITTI frame count is zero or detection rate \u003c 90%.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Shared venv","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"build/.venv_optimum","type":"text","marks":[{"type":"code_inline"}]},{"text":" reused across all models. Never create per-model venvs.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"trtexec ","type":"text","marks":[{"type":"strong"}]},{"text":"--noDataTransfers","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — GPU-only compute matches DeepStream's GPU-to-GPU data flow.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report HTML+PDF","type":"text","marks":[{"type":"strong"}]},{"text":" — always use ","type":"text"},{"text":".claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py","type":"text","marks":[{"type":"code_inline"}]},{"text":". Never write a custom HTML generator or call ","type":"text"},{"text":"wkhtmltopdf","type":"text","marks":[{"type":"code_inline"}]},{"text":" directly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Object detection only","type":"text","marks":[{"type":"strong"}]},{"text":" — reject non-detection architectures from ","type":"text"},{"text":"config.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" before building anything.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Encoder fallback (MANDATORY)","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"x264enc","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"openh264enc","type":"text","marks":[{"type":"code_inline"}]},{"text":" are ","type":"text"},{"text":"prohibited","type":"text","marks":[{"type":"strong"}]},{"text":". On NVENC-unavailable systems, use ","type":"text"},{"text":"theoraenc + oggmux","type":"text","marks":[{"type":"code_inline"}]},{"text":" (LGPL; ships in gst-plugins-base; output is ","type":"text"},{"text":".ogv","type":"text","marks":[{"type":"code_inline"}]},{"text":"). If ","type":"text"},{"text":"theoraenc","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"oggmux","type":"text","marks":[{"type":"code_inline"}]},{"text":" are absent, skip video creation (","type":"text"},{"text":"DS_SINGLE_STREAM_MODE=skipped","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Report which mode was used: ","type":"text"},{"text":"nvv4l2h264enc","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"theoraenc-fallback","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"skipped","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Video source (MANDATORY)","type":"text","marks":[{"type":"strong"}]},{"text":" — default is always ","type":"text"},{"text":"sample_720p.mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":" (1280×720). Never autonomously substitute ","type":"text"},{"text":"sample_1080p_h264.mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":" or any other file. Only use a different video when the user explicitly provides a path (via ","type":"text"},{"text":"DS_VIDEO","type":"text","marks":[{"type":"code_inline"}]},{"text":" env var or script argument).","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Examples","type":"text"}]},{"type":"paragraph","content":[{"text":"Default model, end to end.","type":"text","marks":[{"type":"strong"}]},{"text":" Bootstrap once, then run the full pipeline:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"docker run --rm -it --gpus all --shm-size=16g -v \"$PWD\":/work -w /work \\\n --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\\n .claude/skills/deepstream-import-vision-model/setup.sh\n# then: \"Use deepstream-import-vision-model to run PekingU/rtdetr_r50vd\"","type":"text"}]},{"type":"paragraph","content":[{"text":"SafeTensors model with no published ONNX.","type":"text","marks":[{"type":"strong"}]},{"text":" Step 2b exports it first; the wrapper reports which backend produced the graph and fails loudly if the batch dimension was baked in:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \\\n models/$MODEL_NAME/hf_model models/$MODEL_NAME/onnx_export/\n# [export] backend=dynamo\n# [export] dynamo produced a static batch dimension; trying the next backend\n# [export] backend=legacy-torchscript\n# [export] pixel_values shape=['batch', 3, 640, 640]","type":"text"}]},{"type":"paragraph","content":[{"text":"Pin a Hub revision","type":"text","marks":[{"type":"strong"}]},{"text":" for a reproducible build — any exporter flag passes straight through:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \\\n PekingU/rtdetr_r50vd models/rtdetr/onnx_export --revision \u003ccommit-sha> --opset 18","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pipeline Timing","type":"text"}]},{"type":"paragraph","content":[{"text":"Wrap every step:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"STEP_START=$(date +%s.%N)\n# ... step commands ...\nSTEP_END=$(date +%s.%N)\nSTEP_DURATION=$(python3 -c \"print(round($STEP_END - $STEP_START, 2))\") # bc is not in the container; python3 always is\necho \"[Step N] completed in ${STEP_DURATION}s\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Track ","type":"text"},{"text":"PIPELINE_START","type":"text","marks":[{"type":"code_inline"}]},{"text":" (before Step 1) and ","type":"text"},{"text":"PIPELINE_END","type":"text","marks":[{"type":"code_inline"}]},{"text":" (after Step 8). Report all durations in the benchmark report.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Report Output (MANDATORY — all 3 formats)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"benchmark_report.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — markdown source (12 mandatory sections)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"benchmark_report.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" — styled HTML (charts base64-inlined, no local file access)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"benchmark_report_{model_name}.pdf","type":"text","marks":[{"type":"code_inline"}]},{"text":" — via ","type":"text"},{"text":"md-to-html-pdf.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"; verify charts are embedded by counting ","type":"text"},{"text":"data:image/png","type":"text","marks":[{"type":"code_inline"}]},{"text":" occurrences in the HTML output: ","type":"text"},{"text":"grep -o 'data:image/png' benchmark_report.html | wc -l","type":"text","marks":[{"type":"code_inline"}]},{"text":" should equal 5","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Run charts and report scripts with the shared venv active: ","type":"text"},{"text":"source build/.venv_optimum/bin/activate","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Documents","type":"text"}]},{"type":"paragraph","content":[{"text":"IMPORTANT","type":"text","marks":[{"type":"strong"}]},{"text":": Read the relevant reference before starting each phase. Do NOT generate code from memory.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Document","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use When","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/model-acquire.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/model-acquire.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Steps 1–3: HF/NGC URL parsing, format detection, ONNX download, SafeTensors export, label extraction","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/engine-build.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/engine-build.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Steps 4–5: trtexec engine build, benchmarks, PEAK_GPU_STREAMS derivation, iterative scaling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/pipeline-run.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/pipeline-run.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Steps 6–7: custom bbox parser, nvinfer config, single-stream validation, KITTI dump, multi-stream benchmark","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/report-generation.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/report-generation.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 8: benchmark_data.json, 5 charts, 12-section markdown report, HTML + PDF","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scripts","type":"text"}]},{"type":"paragraph","content":[{"text":"Installed into ","type":"text"},{"text":".claude/skills/deepstream-import-vision-model/scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" by ","type":"text"},{"text":"install.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/hf-list-files.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List HuggingFace repo files","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/hf-download-config.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download config.json from HF","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/ngc-list-files.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List NGC model files","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/ngc-download.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download NGC model archive","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/safetensors-to-onnx.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Export SafeTensors → ONNX via ","type":"text"},{"text":"torch.onnx.export","type":"text","marks":[{"type":"code_inline"}]},{"text":" (wrapper)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/safetensors_to_onnx.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The exporter — dynamo backend, TorchScript fallback, verifies dynamic batch","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/inspect-onnx.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1–5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inspect ONNX input/output shapes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/make-static-batch-onnx.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4–5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bake batch dim into ONNX","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model/cleanup.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Any","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Remove staging dirs, preserve shared venv","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"engine/benchmark-trtexec.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4–5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run trtexec with standard flags","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deepstream/ds-single-stream.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6–7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Single-stream visual validation (NVENC primary; theoraenc+oggmux fallback; skip if neither)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deepstream/ds-sweep.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6–7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2-phase batch size sweep","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deepstream/benchmark-ds.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6–7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fixed-stream DS benchmark","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deepstream/ds-kitti-dump.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6–7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KITTI detection dump via deepstream-app","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deepstream/ds-perf-run.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 7c two-run benchmark — wraps ","type":"text"},{"text":"deepstream-app","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"enable-perf-measurement=1","type":"text","marks":[{"type":"code_inline"}]},{"text":", writes fixed-name log for the report parser","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deepstream/extract-frame.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6–7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract sample frames from output video (","type":"text"},{"text":".mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":" NVENC path or ","type":"text"},{"text":".ogv","type":"text","marks":[{"type":"code_inline"}]},{"text":" theoraenc fallback)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/generate-benchmark-charts.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate 5 benchmark PNG charts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/md-to-html-pdf.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Markdown → styled HTML → PDF (canonical benchmark report path)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/md-to-pdf.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Any","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Markdown → PDF via pandoc/pdflatex — for design docs and references only, NOT for benchmark reports (use md-to-html-pdf.py for those)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/report-style.css","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CSS for HTML report","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/render-mermaid-for-pdf.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mermaid diagram → PNG","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/mermaid-puppeteer.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vetted Puppeteer config for Mermaid (sandboxed; non-root)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report/mermaid-puppeteer-root.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vetted Puppeteer config for Mermaid (used when running as root)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Error Reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tilted/diagonal bounding boxes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Parser struct not zero-initialized — use ","type":"text"},{"text":"NvDsInferObjectDetectionInfo obj = {};","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Zero KITTI files","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"gie-kitti-output-dir","type":"text","marks":[{"type":"code_inline"}]},{"text":" not read by nvinfer — use ","type":"text"},{"text":"ds-kitti-dump.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (wraps ","type":"text"},{"text":"deepstream-app","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Engine rebuilds every DS run","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model-engine-file","type":"text","marks":[{"type":"code_inline"}]},{"text":" path wrong — check relative path from ","type":"text"},{"text":"config/","type":"text","marks":[{"type":"code_inline"}]},{"text":" dir","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"setDimensions","type":"text","marks":[{"type":"code_inline"}]},{"text":" negative dims","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"infer-dims=3;H;W","type":"text","marks":[{"type":"code_inline"}]},{"text":" to nvinfer config for dynamic ONNX models","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--memPoolSize","type":"text","marks":[{"type":"code_inline"}]},{"text":" workspace 0.03 MiB","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"M","type":"text","marks":[{"type":"code_inline"}]},{"text":" suffix not ","type":"text"},{"text":"MiB","type":"text","marks":[{"type":"code_inline"}]},{"text":" — e.g. ","type":"text"},{"text":"--memPoolSize=workspace:32768M","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ForeignNode build failure (DETR)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"onnxsim","type":"text","marks":[{"type":"code_inline"}]},{"text":" — see references/engine-build.md. Not reproduced on TRT 10.16 with either export backend","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ONNX has a static batch dim","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Both export backends specialized it — see the gotchas in references/model-acquire.md","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Zero detections","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wrong ","type":"text"},{"text":"net-scale-factor","type":"text","marks":[{"type":"code_inline"}]},{"text":" — check model family table in references/pipeline-run.md","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No module named 'pyservicemaker'","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install into venv: ","type":"text"},{"text":"pip install /opt/nvidia/deepstream/.../pyservicemaker*.whl","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"deepstream-import-vision-model","tags":["deepstream","tensorrt","object-detection","import-vision-model"],"author":"@skillopedia","source":{"stars":3825,"repo_name":"skills","origin_url":"https://github.com/NVIDIA/skills/blob/HEAD/skills/deepstream-import-vision-model/SKILL.md","repo_owner":"NVIDIA","body_sha256":"7fdc7ebd80e03cc64291ac1c9fc2701e9d12579dc31bf17569752c2a62f4639e","cluster_key":"b00d37cde2772ad0a95197749f004d5561a15129b5399e8e9eee908feca7b26d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"NVIDIA/skills/skills/deepstream-import-vision-model/SKILL.md","text_bytes":336608,"source_commit":"25340619336e89bcc40365c28bfe9e790430468e","skills_sh_rank":7811,"attachment_count":50,"text_attachments":50,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/deepstream-import-vision-model/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"ai-agent-development","category_label":"AI"},"exact_dupes_collapsed_into_this":0},"license":"CC-BY-4.0 AND Apache-2.0","version":"v1","category":"ai-agent-development","metadata":{"tags":["deepstream","tensorrt","object-detection","import-vision-model"],"team":"deepstream-sdk","owner":"Tushar Khinvasara \[email protected]>","author":"Tushar Khinvasara \[email protected]>","domain":"computer-vision","service":"deepstream","version":"1.5.2","reviewed":"2026-08-04","languages":["bash","python","cpp"]},"import_tag":"skills-sh-top20k-2026-11","description":"Use this skill to bring a supported object-detection vision model from HuggingFace or NVIDIA NGC into an NVIDIA DeepStream pipeline with end-to-end automation: ONNX download, SafeTensors export, TRT engine build, custom nvinfer bbox parser, multi-stream benchmark, and PDF report. Object detection models only.\n"}},"renderedAt":1793671507720}

DeepStream Import Vision Model When this skill is active, read the relevant reference document before starting each phase . Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in . Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the us…