NV Model Acquire — Steps 1-3
Acquire an ONNX model from Hugging Face, creating the mandatory model folder structure.
Intake — choose the model
Before Step 1, present two explicit choices:
- Default model (recommended):
PekingU/rtdetr_r50vdfrom Hugging Face. - Custom object-detection model: collect a Hugging Face ID/URL or a versioned NVIDIA NGC catalog URL.
Do not replace this with only an open-ended source prompt. If Default is selected, set
INPUT="PekingU/rtdetr_r50vd". If Custom is selected, require the source before continuing.
Dry runs show the choices but perform no browsing, downloads, Docker launches, or file writes.
MANDATORY: Model Folder Structure
Create this layout at the start of Step 2 (once $MODEL_NAME is set by Step 1):
models/{model_name}/
model/ config/ parser/ scripts/
benchmarks/engines/
reports/charts/ samples/mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,reports/charts,samples}Temporary staging dirs (hf_model/, ngc_download/, build/) are created inline where needed and cleaned up afterward — they are NOT part of this structure.
Step 1: Parse the Model Source URL
Accept a model URL or ID in one of these formats and extract the required fields:
[ -z "$ARGUMENTS" ] && { echo "ERROR: No model URL or ID provided. Usage: /deepstream-import-vision-model <url>"; exit 1; }
INPUT="${ARGUMENTS}"
if echo "$INPUT" | grep -q "catalog.ngc.nvidia.com"; then
# NGC catalog URL
# e.g. https://catalog.ngc.nvidia.com/orgs/nvidia/teams/tao/models/trafficcamnet_transformer_lite/files?version=deployable_resnet50_v2.0
MODEL_SOURCE="ngc"
NGC_ORG=$(echo "$INPUT" | sed 's|.*/orgs/\([^/]*\)/.*|\1|')
NGC_TEAM=$(echo "$INPUT" | sed 's|.*/teams/\([^/]*\)/.*|\1|')
MODEL_NAME=$(echo "$INPUT" | sed 's|.*/models/\([^/]*\)/.*|\1|')
NGC_VERSION=$(echo "$INPUT" | sed 's|.*version=\([^&]*\).*|\1|')
echo "Source: NGC Org: $NGC_ORG Team: $NGC_TEAM Model: $MODEL_NAME Version: $NGC_VERSION"
else
# HuggingFace full URL or short ID (e.g. https://huggingface.co/onnx-community/yolov8n or onnx-community/yolov8n)
MODEL_SOURCE="hf"
SLUG=$(echo "$INPUT" | sed 's|https://huggingface.co/||' | sed 's|/resolve/.*||' | sed 's|/$||')
HF_ORG=$(echo "$SLUG" | cut -d/ -f1)
MODEL_NAME=$(echo "$SLUG" | cut -d/ -f2)
echo "Source: HF Org: $HF_ORG Model: $MODEL_NAME"
fiMODEL_SOURCE(hforngc) drives category selection in Step 2MODEL_NAMEis used as the folder name throughout (models/{MODEL_NAME}/)- Proceed to Step 2 with these variables set
Step 2: Detect Model Source and Format
First, create the model directory structure (required for all sources), then route by source:
# Create permanent model directory structure (all sources — HF and NGC)
mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,reports/charts,samples}
# Route based on MODEL_SOURCE set in Step 1
if [ "$MODEL_SOURCE" = "ngc" ]; then
echo "NGC model detected — skipping HF repo browse, proceeding to Step 2d"
# Skip to Step 2d directly — do not run any HF curl commands below
fi
# The following HF browse, config download, and labels extraction only runs for MODEL_SOURCE=hfBrowse the HF repository and classify available model files using the vetted helper script (validates inputs, uses HTTPS+TLSv1.2 only, honors
$HF_TOKEN):FILES="$(bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-list-files.sh "$HF_ORG" "$MODEL_NAME")" ONNX_FILES=$(echo "$FILES" | grep -E '\.onnx$' || true) ST_FILES=$(echo "$FILES" | grep -E '\.(safetensors|bin)$' || true) echo "ONNX files: ${ONNX_FILES:-none}" echo "SafeTensors/bin: ${ST_FILES:-none}" echo "All files: $FILES" # If ONNX list is empty in root, also check /onnx subdirectory if [ -z "$ONNX_FILES" ]; then ONNX_SUB="$(bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-list-files.sh "$HF_ORG" "$MODEL_NAME" onnx | grep -E '\.onnx$' || true)" echo "ONNX in /onnx subdir: ${ONNX_SUB:-none}" fiClassify the repo into one of these categories:
Category A: ONNX files available -> proceed to Step 2a (select ONNX variant) Category B: SafeTensors/PyTorch only (no ONNX) -> proceed to Step 2b (export to ONNX) Category C: No usable model files -> inform user, suggest alternative repos Category D: NGC model (not on HuggingFace) -> proceed to Step 2d (NGC download)
Download
config.json— required for architecture detection and label extraction. Uses the vetted helper script (validated inputs, HTTPS+TLS, honors$HF_TOKEN):# HF: download from API via vetted helper. NGC: extracted from archive in Step 2d. if [ "$MODEL_SOURCE" = "hf" ]; then bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-download-config.sh \ "$HF_ORG" "$MODEL_NAME" "models/$MODEL_NAME/config/config.json" else echo "NGC model — config.json will be extracted from the downloaded archive in Step 2d" fi # Note: models/$MODEL_NAME/config/ already exists from the MANDATORY mkdir at the top of Step 2Inspect
config.jsonto identify:- Model type (e.g.,
grounding-dino,detr,yolos,resnet,swin) - Architecture class (e.g.,
GroundingDinoForObjectDetection) - Number of inputs (single input vs multi-modal)
- Model type (e.g.,
Reject non-detection architectures (fail fast): Check the
architecturesfield inconfig.jsonbefore continuing. If the architecture class ends in a non-detection suffix such asForImageClassification,ForSemanticSegmentation,ForInstanceSegmentation,ForPanopticSegmentation,ForDepthEstimation,ForMaskedLM,ForTokenClassification, orForCausalLM, 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 inForObjectDetection(or, for some DETR-family variants,ForConditionalDetection/ForZeroShotObjectDetection).Validate the architecture and extract
labels.txt— one shared helper does both, so the HF and NGC routes cannot drift apart. Run it as soon asconfig.jsonis in place (for HF that is now; for NGC it runs at the end of Step 2d):build/.venv_optimum/bin/python \ .claude/skills/deepstream-import-vision-model/scripts/model/config-to-labels.py \ --config models/$MODEL_NAME/config/config.json \ --labels models/$MODEL_NAME/config/labels.txtIt exits non-zero on a non-detection architecture (the fail-fast gate above) and on a missing label map. Treat either as fatal — do not prompt the user, and never fall back to hardcoded COCO, ImageNet, or any other default list.
Step 2a: Select ONNX Variant (Category A)
- Identify available quantization variants (fp32, fp16, int8, int4, quantized, etc.)
- Default preference: fp16. Apply this logic:
- If fp16 variant exists -> select it silently, log:
"Selected: fp16 (default). All available: [list]" - 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. - If only one ONNX file exists -> log it and proceed without asking
- If fp16 variant exists -> select it silently, log:
- Construct the resolved download URL for the selected variant from the tree listing:
# The tree API returns entries with a "path" field (relative to repo root) # Construct the download URL as: PATH_FROM_TREE="<path field from tree listing, e.g. onnx/model_fp16.onnx>" ONNX_URL="https://huggingface.co/$HF_ORG/$MODEL_NAME/resolve/main/$PATH_FROM_TREE" # Example: path="onnx/model_fp16.onnx" -> URL ends in /resolve/main/onnx/model_fp16.onnx # Store this URL for use in Step 3 - After URL construction, proceed to Step 3 (download ONNX)
Step 2b: Export SafeTensors to ONNX (Category B)
When 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.
2b-i: Virtual Environment (already built by setup.sh, in-container)
- The shared venv at
build/.venv_optimumis created inside the DeepStream container by the skill'ssetup.sh(one-time bootstrap) — do not create it on the host. Every export command runs in-container against it; usePY=build/.venv_optimum/bin/pythonandbuild/.venv_optimum/bin/optimum-cli. - It's a single shared venv across all models (
torch/transformers/onnxruntimeare heavy and identical model-to-model). If it's missing, run the bootstrap (from SKILL.md Pre-flight):# in-container (via docker run … --entrypoint bash … setup.sh); NOT on the host .claude/skills/deepstream-import-vision-model/setup.sh # builds build/.venv_optimum + wkhtmltopdf - For a new model that needs extra packages (e.g.
timmfor DETR-family backbones oronnxsim),pip installthem into the existing shared venv rather than creating a new one:source build/.venv_optimum/bin/activate pip install timm # or: pip install onnxsim - The venv lives under
build/.venv_optimumat the repo root, keepingmodels/clean and excluded from git via the root.gitignore - All subsequent Python/pip commands in Step 2b must run inside this venv
- Legacy per-model venvs at
build/.venv_$MODEL_NAMEfrom older runs are still cleaned up by.claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME"for backward compatibility
2b-ii: Download Required Files
- Download from the HF repo into
models/$MODEL_NAME/hf_model/using-Pto avoid changing the working directory:mkdir -p models/$MODEL_NAME/hf_model HF_BASE="https://huggingface.co/$HF_ORG/$MODEL_NAME/resolve/main" # Download model files wget -P models/$MODEL_NAME/hf_model "$HF_BASE/model.safetensors" wget -P models/$MODEL_NAME/hf_model "$HF_BASE/config.json" wget -P models/$MODEL_NAME/hf_model "$HF_BASE/preprocessor_config.json" # For text+vision models, also download tokenizer files (failures are non-fatal): wget -P models/$MODEL_NAME/hf_model "$HF_BASE/tokenizer.json" || true wget -P models/$MODEL_NAME/hf_model "$HF_BASE/tokenizer_config.json" || true wget -P models/$MODEL_NAME/hf_model "$HF_BASE/vocab.txt" || true wget -P models/$MODEL_NAME/hf_model "$HF_BASE/special_tokens_map.json" || true - For sharded models (multiple
.safetensorsfiles), also downloadmodel.safetensors.index.jsonand all shards
2b-iii: SafeTensors -> ONNX Export -- Max 3 Retries
optimum was removed from this skill. It pinned
transformersbelow 4.54.0, which blocked the releases fixing its RCE advisories (GHSA-29pf-2h5f-8g72, fixed in 5.3.0; GHSA-fgcw-684q-jj6r, fixed in 5.5.0), and optimum 2.1.0 dropped theonnxsubcommand entirely. Export now goes throughtorch.onnx.exportdirectly.
- Run the export wrapper (it resolves the shared venv and calls
safetensors_to_onnx.py):bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \ models/$MODEL_NAME/hf_model \ models/$MODEL_NAME/onnx_export/ - It writes
models/$MODEL_NAME/onnx_export/model.onnxand:- reads the export resolution from
preprocessor_config.json(falling back to 640x640); - rejects non-detection architectures before doing any work;
- tries the dynamo backend, then falls back to TorchScript if dynamo specialized the batch
dimension — it prints
[export] backend=...so you can see which one produced the graph; - consolidates any sidecar
model.onnx.databack into the.onnx; - verifies the graph has the
pixel_valuesinput pluslogits/pred_boxesoutputs, and that the batch dimension really is dynamic. It exits non-zero rather than emitting a static-batch graph, because DeepStream needsbatch_size == num_streamsup toMAX_BS.
- reads the export resolution from
- Expected output for the default model — note the automatic fallback:
[export] backend=dynamo [export] pixel_values shape=[2, 3, 640, 640] [export] dynamo produced a static batch dimension; trying the next backend [export] backend=legacy-torchscript [export] pixel_values shape=['batch', 3, 640, 640] - Useful flags (passed straight through):
--opset N·--image-size N·--max-batch N·--static-batch·--legacy-torchscript - If export succeeds, copy the ONNX file to the
model/subdirectory:cp models/$MODEL_NAME/onnx_export/model.onnx models/$MODEL_NAME/model/$MODEL_NAME.onnx - Retry policy: the two-backend fallback is automatic, so a failure here means both backends
failed. Retry up to 3 times total with adjustments between attempts:
- Retry 1: Set
--image-sizeexplicitly if the error mentions a shape or resize mismatch - Retry 2: Try a different
--opset(18 is the default; 17 and below fail onResize) - Retry 3: Try
--static-batchif both backends baked in the batch dimension. The engine must then be built at a fixed batch size — seereferences/engine-build.md. - After 3 failed attempts, fall back to Step 2b-iv (hand-written export for an unusual architecture)
- Retry 1: Set
2b-iv: Fallback -- Hand-written torch.onnx.export (unusual architectures) -- Max 3 Retries
- If the wrapper fails after 3 retries because the model does not follow the standard
logits/pred_boxesdetection contract, write the export inline and adjust the wrapper outputs:build/.venv_optimum/bin/python -c " import torch from transformers import AutoModelForObjectDetection model = AutoModelForObjectDetection.from_pretrained('models/$MODEL_NAME/hf_model').eval() class Wrap(torch.nn.Module): def __init__(s, m): super().__init__(); s.m = m def forward(s, x): o = s.m(pixel_values=x) return o.logits, o.pred_boxes # <- adjust for the architecture # Dummy input matching preprocessor_config.json dimensions dummy = torch.randn(1, 3, 800, 800) # TorchScript backend — the more reliable one for a dynamic batch dimension on # DETR-family detectors. Swap to the dynamo block below if this backend fails. torch.onnx.export(Wrap(model), dummy, 'models/$MODEL_NAME/model/$MODEL_NAME.onnx', opset_version=18, do_constant_folding=True, dynamo=False, input_names=['pixel_values'], output_names=['logits', 'pred_boxes'], dynamic_axes={'pixel_values': {0: 'batch'}, 'logits': {0: 'batch'}, 'pred_boxes': {0: 'batch'}}) # dynamo backend (use for text-conditioned models the tracer cannot handle): # batch = torch.export.Dim('batch', min=1, max=16) # torch.onnx.export(Wrap(model), (torch.randn(2, 3, 800, 800),), '<out>.onnx', # opset_version=18, dynamo=True, # input_names=['pixel_values'], output_names=['logits', 'pred_boxes'], # dynamic_shapes={'pixel_values': {0: batch}}) " - Adjust the wrapper's returned tensors, input/output names, and shapes for the architecture
- Always confirm the batch dimension survived:
build/.venv_optimum/bin/python -c " import onnx; d = onnx.load('models/$MODEL_NAME/model/$MODEL_NAME.onnx').graph.input[0].type.tensor_type.shape.dim print([x.dim_param or x.dim_value for x in d])" # expect ['batch', 3, H, W] - Retry policy: If manual export fails, retry up to 3 times total with adjustments:
- Retry 1: Try a different
AutoModelclass, or return different tensors from the wrapper - Retry 2: Switch backend (TorchScript <-> dynamo), remembering
dynamic_axesvsdynamic_shapes - Retry 3: Drop the dynamic batch entirely and build a fixed-batch engine
- After 3 failed attempts, stop and generate a failure report
- Retry 1: Try a different
Gotchas for recent PyTorch/transformers (verified on torch 2.13.0 + transformers 5.14.1 inside
nvcr.io/nvidia/deepstream:9.1-triton-multiarch):
- Neither backend works for every architecture — try
dynamo=Truefirst, fall back todynamo=False.safetensors_to_onnx.pydoes this automatically and reports which backend won.dynamo=Truecan silently produce a STATIC batch dimension. The model's own code may specializeshape[0];torch.exportthen 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 withonnx.load(); the exporter asserts it.dynamo=Falsedoes NOT crash on transformers 5.5+ for vision detectors. An earlier revision of this document claimed it did, viacreate_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.- Under
dynamo=Truethe parameter isdynamic_shapes(withtorch.export.Dim), notdynamic_axes; underdynamo=Falseit isdynamic_axes. Passing the wrong one is silently ineffective.- 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.- Call
.eval()on the wrapper module too, not just the loaded model — a freshnn.Moduledefaults to training mode, which changes dropout/batchnorm behaviour during export.- External data files:
torch.onnx.exportmay producemodel.onnx.dataalongside the.onnx. Consolidate before TRT conversion:m = onnx.load(path, load_external_data=True); onnx.save(m, consolidated_path). The exporter does this automatically.- No
ForeignNodefailure was observed for RT-DETR on TRT 10.16 with either backend — seereferences/engine-build.mdfor the historical issue.
2b-v: Handle Multi-Modal Models (e.g., Grounding DINO)
- Models that take both image AND text inputs need special handling for DeepStream (nvinfer only supports image input)
- Strategy: freeze the text prompt into the ONNX graph as a constant
- Run the model once with a fixed text prompt (e.g., "person . car . truck .")
- Export ONNX with the text embeddings baked in as constants
- The resulting ONNX model only needs
pixel_valuesas input
- If freezing is not possible, check
onnx-community/for pre-converted single-input versions - Inform the user about the frozen text prompt and its implications (fixed detection classes)
2b-vi: onnxsim — Run After Export When Needed
If the model has dynamic shape paths that cause TRT ForeignNode fusion issues, simplify the ONNX graph with onnxsim before engine building:
source build/.venv_optimum/bin/activate
pip install onnxsim
python3 -m onnxsim \
models/$MODEL_NAME/model/$MODEL_NAME.onnx \
models/$MODEL_NAME/model/${MODEL_NAME}_sim.onnx
# Use the _sim.onnx for engine building if the original triggers ForeignNode errorsOnly run onnxsim if TRT build fails with ForeignNode warnings — it is not needed for most models.
2b-vii: Validate ONNX Output
- After export, validate the ONNX file:
source build/.venv_optimum/bin/activate python3 -c " import onnx m = onnx.load('models/$MODEL_NAME/model/$MODEL_NAME.onnx') onnx.checker.check_model(m) print('Inputs:') for i in m.graph.input: dims = [d.dim_param or d.dim_value for d in i.type.tensor_type.shape.dim] print(f' {i.name}: {dims}') print('Outputs:') for o in m.graph.output: dims = [d.dim_param or d.dim_value for d in o.type.tensor_type.shape.dim] print(f' {o.name}: {dims}') print('ONNX validation passed!') " - Verify:
- Single image input (no text/mask inputs -- remove if needed)
- Output shapes match expected detection format
- Dynamic batch dimension is present
2b-viii: Cleanup
- Deactivate the venv after export is complete:
deactivate - Keep
build/.venv_optimumacross runs — it is shared by every SafeTensors → ONNX export and rebuilding it for each model costs minutes and GBs.cleanup.shintentionally does not remove it. cleanup.shremoves per-model artifacts (models/$MODEL_NAME/hf_model,models/$MODEL_NAME/onnx_export, and any legacybuild/.venv_$MODEL_NAMEleft over from older runs):# Validated script; will refuse unsafe paths. Shared .venv_optimum is preserved. bash .claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME" # Preview without removing: # bash .claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME" --dry-run- The ONNX file is now at
models/$MODEL_NAME/model/$MODEL_NAME.onnx-- proceed to engine building
Step 2d: NGC Model Download (Category D)
When the model comes from NVIDIA NGC (not HuggingFace), download using the ngc CLI if available, or fall back to wget for direct file download:
# Vetted helper: prefers ngc CLI if installed, else falls back to authenticated
# HTTPS+TLS via curl against the public NGC catalog API. All inputs validated
# against ^[A-Za-z0-9._-]+$. See .claude/skills/deepstream-import-vision-model/scripts/model/ngc-download.sh for details.
bash .claude/skills/deepstream-import-vision-model/scripts/model/ngc-download.sh \
"$NGC_ORG" "$NGC_TEAM" "$MODEL_NAME" "$NGC_VERSION" \
"models/$MODEL_NAME/ngc_download"
# Inspect downloaded files
echo "Downloaded files:"
ls -lhR models/$MODEL_NAME/ngc_download/- Identify the ONNX file(s) in the downloaded archive (often inside a subdirectory named after the model version)
- If the download contains a
.etltor.enginefile 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) - Copy the ONNX to the model directory:
NGC_ONNX=$(find models/$MODEL_NAME/ngc_download -name "*.onnx" | head -1) cp "$NGC_ONNX" models/$MODEL_NAME/model/$MODEL_NAME.onnx echo "ONNX: $NGC_ONNX -> models/$MODEL_NAME/model/$MODEL_NAME.onnx" - Extract
config.jsonfrom the archive and buildlabels.txt(same logic as HF path):NGC_CONFIG=$(find models/$MODEL_NAME/ngc_download -name "config.json" | head -1) if [ -z "$NGC_CONFIG" ]; then echo "ERROR: config.json not found in NGC archive — cannot create labels.txt" echo "Cannot proceed without a label map — aborting. Provide an NGC archive that contains config.json." exit 1 else cp "$NGC_CONFIG" models/$MODEL_NAME/config/config.json echo "config.json extracted from: $NGC_CONFIG" # Same helper the HF route uses: gates the architecture, then writes labels.txt. # The NGC route reaches config.json later than the HF route, so without this a # classification model would build an engine and a parser before anything noticed. build/.venv_optimum/bin/python \ .claude/skills/deepstream-import-vision-model/scripts/model/config-to-labels.py \ --config models/$MODEL_NAME/config/config.json \ --labels models/$MODEL_NAME/config/labels.txt || exit 1 fi
Step 3: Download the ONNX Model
The model directory structure was already created in the MANDATORY block at the top. Do NOT run mkdir -p again here — just download the file:
wget -O "models/$MODEL_NAME/model/$MODEL_NAME.onnx" "${ONNX_URL}"Where $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.
- Also download any external data files if the ONNX model references them (files with
.onnx_dataextension or similar) - Verify the download completed successfully and report file size
Timing
Record wall-clock time at the start and end of this skill:
STEP_START=$(date +%s.%N)
# ... all steps ...
STEP_END=$(date +%s.%N)
STEP_DURATION=$(python3 -c "print(round($STEP_END - $STEP_START, 2))") # bc is not in the container; python3 always isOutput Summary
When complete, print:
=== HF Model Acquire Complete === [Steps 1-3: ${STEP_DURATION}s]
Model: $MODEL_NAME
ONNX: models/$MODEL_NAME/model/$MODEL_NAME.onnx ({size} MB)
Input: {input_name} {input_shape}
Output: {output_names} {output_shapes}
Labels: {num_classes} classes -> models/$MODEL_NAME/config/labels.txt
Ready for: Steps 4-5 — read references/engine-build.md models/$MODEL_NAME/model/$MODEL_NAME.onnx({size}, {input_name}, {input_shape}, {output_names}, {output_shapes}, {num_classes} are filled from the ONNX inspection output — all other fields use bash variables.)