option
MaisonMaison Skill Gestion de base de données vss-generate-video-report

vss-generate-video-report

NVIDIA/skills NVIDIA/skills

Génère des rapports d'analyse vidéo en acheminant les données vers un backend VLM pour une analyse clip par clip ou vers un backend d'analyse pour des rapports portant sur une période donnée, avec vérification du profil de déploiement et réécriture d'URL.

...Développer tout
2
Heure mise à jour 27 septembre 2026

Rapport

Générez un rapport d'analyse vidéo en le redirigeant vers l'un des deux backends — jamais via la méthode POST /generate sur l'agent VSS.

Mode Backend
A. Extrait vidéo /vss-manage-video-io-storage → URL du clip → chat/compléments VLM
B. Plage d’incidents /vss-query-analytics → liste des incidents → rapport narratif

Si la requête est ambiguë (par exemple « rapport sur » sans plage horaire ni description d’incident), utilisez par défaut le mode A. Ne posez la question que si l’utilisateur mentionne à la fois un capteur et une plage horaire. Consultez les exemples ci-dessous pour connaître les formulations de requêtes qui orientent vers chaque mode.

Instructions

  1. Choisissez le mode — Mode A pour un seul clip enregistré ou une vidéo de capteur, Mode B lorsque la requête précise une plage horaire ou des incidents/alertes (comparez avec les exemples).
  2. Vérifiez le profil de déploiement correspondant à ce mode dans la section « Conditions préalables au déploiement » ; transférez la demande à /vss-deploy-profile si la vérification échoue.
  3. Exécutez les étapes numérotées de ce mode — Mode A ou Mode B ci-dessous.
  4. Réécrivez chaque URL de clip destinée à l’utilisateur en utilisant la chaîne $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT (URL de clip lisible dans un navigateur) avant de l’intégrer dans le rapport.
  5. Renvoyer le rapport au format Markdown généré à l'utilisateur.

Spécifications pour les évaluateurs :

  • Le titre principal du mode A DOIT être exactement # Rapport d’analyse vidéo.
  • Le titre principal du mode B DOIT être exactement # Rapport sur la période d’incidents (jamais # Rapport d’incident ni aucune variante mentionnant le nom d’un capteur).
  • Le mode B DOIT inclure la section ## Informations de base avec les lignes exactes requises du modèle (Identifiant du rapport, Plage, Portée, Nombre total d’incidents, Confirmés / Rejetés / Non vérifiés).

Exemples

  • « Générer un rapport pour cette vidéo » / « rapport sur » → Mode A
  • « Analyser warehouse_01.mp4 » / « Créer un rapport d'analyse sur la vidéo téléchargée » → Mode A
  • « Rapport sur les incidents survenus de 12 h 31 Z à 12 h 32 Z » → Mode B
  • « Rapport sur les alertes d’aujourd’hui » / « Quels incidents se sont produits au cours de dernière heure » → Mode B
  • « Résumer les alertes sur entre et » → Mode B

Déclencheurs négatifs

N’ utilisez pas cette compétence lorsque la requête correspond à l’un des cas suivants :

  • Questions-réponses visuelles ponctuelles sur un extrait vidéo qui ne demandent pas explicitement un rapport (« De quelle couleur est le camion ? », « Que se passe-t-il à 00:12 ? ») → utilisez /vss-ask-video.
  • Recherche dans les archives ou par similarité sémantique (« trouver des chariots élévateurs », « rechercher tous les clips montrant des cas de talonnage ») → utilisez /vss-search-archive.
  • Recherche en lecture seule d’incidents/de métriques sans besoin de génération de rapport → utilisez /vss-query-analytics.
  • Déploiement, démantèlement ou modification de profil (« déployer des alertes », « changer de profil », « mettre en service la base ») → utilisez /vss-deploy-profile.
  • Demandes de gestion des alertes et des règles en temps réel → utilisez /vss-manage-alerts.

Ne jamais acheminer les rapports via la méthode POST /generate de l’agent VSS.

Prérequis de déploiement

Le mode A nécessite le profil de base VSS (VST + VLM NIM). Le mode B nécessite le profil d’alertes VSS (VA-MCP + Elasticsearch).

Test :

# Mode A — Accessibilité VST + VLM
curl -sf --max-time 5 "http://${HOST_IP}:30888/vst/api/v1/sensor/version" >/dev/null

# Mode B — VA-MCP
curl -sf --max-time 5 "http://${HOST_IP}:9901/" >/dev/null

Si la sonde échoue, transférez la tâche à /vss-deploy-profile avec l'option -p base (mode A) ou -p alerts (mode B). Confirmez toujours au préalable le déploiement auprès de l'utilisateur.

URL des extraits : entrée VLM vs lien vers le rapport du navigateur

VST renvoie des URL de capture en utilisant l’adresse hôte-port interne à l’agent ${HOST_IP}:30888. Conservez cette URL d’origine comme VIDEO_URL pour les récupérations de trames VLM locales ou au sein du cluster. Ne réécrivez pas l’URL d’entrée VLM uniquement pour la rendre lisible par un navigateur.

Ne créez une BROWSER_CLIP_URL que pour les URL affichées dans le rapport généré. La couche de déploiement exporte l’hôte:port destiné au navigateur sous la forme $VSS_PUBLIC_HOST / $VSS_PUBLIC_PORT (et le schéma sous la forme $VSS_PUBLIC_HTTP_PROTOCOL) dans chaque fichier .env de profil — Brev ou bare-metal —. La réécriture du lien du rapport est donc la suivante :

: "${VSS_PUBLIC_HOST:?Définissez VSS_PUBLIC_HOST avant de réécrire les URL des extraits}"
: "${VSS_PUBLIC_PORT:?Définissez VSS_PUBLIC_PORT avant de réécrire les URL des extraits}"
VSS_PUBLIC_HTTP_PROTOCOL="${VSS_PUBLIC_HTTP_PROTOCOL:-http}"
BROWSER_CLIP_URL=$(echo "$RAW_URL" | sed -E "s|^https?://[^/]+|${VSS_PUBLIC_HTTP_PROTOCOL}://${VSS_PUBLIC_HOST}:${VSS_PUBLIC_PORT}|")

Si l’une des valeurs d’hôte public requises est manquante, omettez le lien vers l’extrait destiné au rapport et indiquez qu’il n’a pas été possible de générer une URL lisible par un navigateur ; ne bloquez pas le chemin d’analyse VLM local. Appliquez la réécriture à chaque URL de clip figurant dans le rapport généré (Mode A, étape 4, ligne « URL du clip » ; Mode B, sous-puce « clip par incident »). Laissez le bloc de contenu video_url du VLM dans le Mode A, étape 3, sur l'URL interne d'origine lorsque le VLM est local ou au sein du cluster.

Mode A — Rapport sur un clip vidéo enregistré

Si le profil VSS lvs est déployé — la commande `curl -sf --max-time 5 "http://${HOST_IP}:38111/v1/ready"` renvoie un code HTTP 200 — exécutez ` /vss-summarize-video ` pour générer le résumé, puis collez sa sortie dans le modèle de rapport à l’étape 4 et ignorez les étapes 1 à 3 (le chemin direct vers le VLM). N’exécutez les étapes 1 à 3 que lorsque /v1/ready renvoie un code autre que 200.

Étape 1 — Résoudre l’URL du clip

Transférez la tâche à /vss-manage-video-io-storage pour :

  1. répertorier les capteurs et vérifier que le capteur nommé (le télécharger d’abord s’il n’existe pas).

  2. Récupérer les données /storage//timelines pour la plage enregistrée lorsque l’utilisateur n’a pas fourni de startTime ni d’endTime.

  3. Demander l’URL du clip :

    curl -s "http://${HOST_IP}:30888/vst/api/v1/storage/file//url?startTime=&endTime=&container=mp4&disableAudio=true" | jq -r .videoUrl
    

    Cela donne une URL mp4 directe à partir de laquelle le VLM local / au sein du cluster peut extraire des images. Associez-la à VIDEO_URL (utilisée par le VLM à l’étape 3) et définissez RAW_URL="$VIDEO_URL" avant d’appliquer la réécriture du lien du rapport pour générer BROWSER_CLIP_URL à l’étape 4 — le navigateur de l’utilisateur ne peut pas accéder directement à $VIDEO_URL. Le mode A nécessite que le point de terminaison VLM sélectionné soit capable de récupérer VIDEO_URL. Les déploiements NIM/RT-VLM locaux en sont généralement capables ; les points de terminaison distants ne peuvent généralement pas récupérer localhost, une adresse HOST_IP privée ou des URL internes à VST. Si le point de terminaison VLM_ENDPOINT en direct est distant, signalez cette exigence d’accessibilité au lieu d’ effectuer une requête de chat qui échouera après la réussite de /v1/models.

Étape 2 — Résolution du point de terminaison VLM et du modèle

Le déploiement peut fournir le VLM via l’une ou l’autre de ces deux piles. Toutes deux exposent une API de chat/complétions compatible OpenAI — choisissez celle qui est opérationnelle :

Backend Variables d’environnement Point de terminaison hôte type Sélectionné lorsque
NIM Cosmos VLM_BASE_URL, VLM_NAME, VLM_MODE, VLM_MODEL_TYPE ${VLM_BASE_URL}/v1 (pas de /v1 à la fin de la variable d’environnement ; l’agent l’ajoute) VLM_MODEL_TYPE ≠ rtvi et VLM_MODE ∈ {local, local_shared, remote} et VLM_BASE_URL n'est pas vide
RT-VLM Cosmos RTVI_VLM_BASE_URL, RTVI_VLM_MODEL_TO_USE, VLM_MODEL_TYPE ${RTVI_VLM_BASE_URL}/v1 — si non définie, dérivée de ${HOST_IP} (http://${HOST_IP}:8018/v1 pour les alertes, http://${HOST_IP}:30082/v1 pour les données de base) VLM_MODEL_TYPE = rtvi, ou VLM_MODE=none, ou VLM_BASE_URL vide ; c’est également le seul chemin d’accès à l’entrepôt

Lire les valeurs en temps réel à partir du conteneur de l’agent en cours d’exécution — ne pas deviner :

docker exec vss-agent sh -lc '
for k in HOST_IP VLM_MODE VLM_MODEL_TYPE VLM_BASE_URL VLM_NAME RTVI_VLM_BASE_URL RTVI_VLM_MODEL_TO_USE; do
  v="$(printenv "$k")"
  [ -n "$v" ] && printf "%s=%s\n" "$k" "$v"
done
'

Ne pas exiger la variable d'environnement RTVI_VLM_ENDPOINT de la part de vss-agent; plusieurs profils ne l'injectent pas.

Règle de sélection :

if [ "${VLM_MODEL_TYPE:-}" = "rtvi" ]; then
  VLM_BACKEND="rtvlm"
  VLM_ENDPOINT="${RTVI_VLM_BASE_URL:+${RTVI_VLM_BASE_URL%/}/v1}"
  [ -z "${VLM_ENDPOINT}" ] && VLM_ENDPOINT="http://${HOST_IP}:8018/v1"   # alertes par défaut
  VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
elif [ -n "${VLM_BASE_URL}" ] && [ "${VLM_MODE}" != "none" ]; then
  VLM_BACKEND="nim_cosmos"
  VLM_ENDPOINT="${VLM_BASE_URL%/}/v1"
  VLM_MODEL="${VLM_NAME}"
else
  VLM_BACKEND="rtvlm"
  VLM_ENDPOINT="${RTVI_VLM_BASE_URL:+${RTVI_VLM_BASE_URL%/}/v1}"
  [ -z "${VLM_ENDPOINT}" ] && VLM_ENDPOINT="http://${HOST_IP}:30082/v1"  # valeur par défaut
  VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
fi

Testez /v1/models avant d'envoyer une requête de chat afin de vérifier que le point de terminaison choisi est opérationnel et que le modèle est chargé :

curl -sf --max-time 5 "${VLM_ENDPOINT}/models" | jq -r '.data[].id'

Si la vérification échoue ou si les identifiants répertoriés n'incluent pas ${VLM_MODEL}, basculez vers l'autre backend (ou signalez l'erreur — ne choisissez jamais en silence un modèle qui ne se trouve pas sur le serveur).

Étape 3 — Appeler directement le VLM

Utilisez le point de terminaison « chat/completions » compatible OpenAI avec un bloc de contenu « video_url » — la même structure de charge utile et les mêmes paramètres multimodaux que ceux utilisés par « video_understanding » dans src/vss_agents/tools/video_understanding.py (_build_vlm_messages + l’appel Cosmos base_vlm.bind(...) ).

L’échantillonnage des images et le budget en jetons visuels (pixels) doivent refléter les paramètres actuels de `video_understanding ` pour le profil actif. Envoyez mm_processor_kwargs et media_io_kwargs afin que l’appel direct utilise le même échantillonnage d’images et le même budget de pixels que l’outil video_understanding intégré à l’agent — les omettre permet au VLM d’appliquer ses propres valeurs par défaut, ce qui fait que la sortie s’écarte du chemin de l’agent.

PROMPT='Décrivez en détail ce qui se passe dans la vidéo, en indiquant les horodatages (début–fin en secondes à partir du début du clip) pour chaque segment ou événement. Mentionnez les scènes, les objets, les personnes, les véhicules et les actions notables.'

# Le raisonnement est désactivé par défaut — ce qui correspond à la configuration « video_understanding » du profil de base (`reasoning: false`).
# video_understanding.py utilise config.reasoning sauf si l’appelant le remplace, donc le raisonnement est désactivé par défaut.
# Ajoutez le suffixe de raisonnement « Cosmos Reason 2 » UNIQUEMENT lorsque l’utilisateur demande explicitement un raisonnement
# (ne l’ajoutez pas pour les VLM autres que « cosmos-reason2 »). Lorsque le raisonnement est désactivé, la réponse ne comporte pas  de bloc .
if [ "${REASONING:-false}" = "true" ]; then
PROMPT="${PROMPT}

Répondez à la question en utilisant le format suivant :


Votre raisonnement.


Écrivez votre réponse finale immédiatement après la balise ."
fi

# Si l’étape 3 est exécutée de manière autonome, déduisez le backend manquant à partir de l’environnement/modèle actuel.
[ -z "${VLM_BACKEND:-}" ] && {
  if [ "${VLM_MODEL_TYPE:-}" = "rtvi" ]; then
    VLM_BACKEND="rtvlm"
  elif [[ "${VLM_MODEL:-}" == nvidia/cosmos* ]]; then
    VLM_BACKEND="nim_cosmos"
  else
    VLM_BACKEND="rtvlm"
  fi
}

# Paramètres multimodaux — à déterminer à partir du chemin d’accès au fichier de configuration de l’agent en direct, et non à partir de valeurs codées en dur.
CFG_JSON=$(
docker exec vss-agent python3 -c '
import json, os, yaml
p = os.getenv("VSS_AGENT_CONFIG_FILE")
if not p:
    raise SystemExit("VSS_AGENT_CONFIG_FILE n’est pas défini dans vss-agent")
if not os.path.isabs(p):
    p = os.path.join("/vss-agent", p.lstrip("./"))
with open(p, encoding="utf-8") as f:
    cfg = yaml.safe_load(f) or {}
vu = (cfg.get("functions", {}) or {}).get("video_understanding", {}) or {}
print(json.dumps({
    "max_fps": int(vu.get("max_fps", 2)),
    "max_frames": int(vu.get("max_frames", 30)),
    "min_pixels": int(vu.get("min_pixels", 3136)),
    "max_pixels": int(vu.get("max_pixels", 8388608)),
}))
')
)
[ -n "${CFG_JSON}" ] || { echo "Échec de la lecture de la configuration video_understanding depuis vss-agent"; exit 1; }
jq -e . >/dev/null <<< "${CFG_JSON}" || { echo "Fichier JSON de configuration invalide provenant de vss-agent" ; exit 1 ; }
MAX_FPS="$(jq -r '.max_fps' <<< "${CFG_JSON}")"
MAX_FRAMES="$(jq -r '.max_frames' <<< "${CFG_JSON}")"
MIN_PIXELS="$(jq -r '.min_pixels' <<< "${CFG_JSON}")"
MAX_PIXELS="$(jq -r '.max_pixels' <<< "${CFG_JSON}")"

# num_frames = min(int(clip_seconds) * max_fps, max_frames), min 1 — correspond à video_understanding.py.
# clip_seconds (Step 1 endTime-startTime) peut comporter une fraction ; troncature à un nombre entier de secondes — bash $((...))
# ne prend que des entiers et renvoie une erreur pour « 15,0 »/« 1,5 ». Par défaut 15 s -> plafonné à MAX_FRAMES.
CLIP_SECONDS=$(awk -v s="${CLIP_SECONDS:-15}" 'BEGIN{printf "%d", s}')
NUM_FRAMES=$(( CLIP_SECONDS * MAX_FPS ))
[ "$NUM_FRAMES" -gt "$MAX_FRAMES" ] && NUM_FRAMES=$MAX_FRAMES
[ "$NUM_FRAMES" -lt 1 ] && NUM_FRAMES=1

# N'appliquer les arguments de ligne de commande mm/media de Cosmos que sur le chemin NIM Cosmos.
# Le mode RT-VLM utilise son propre prétraitement côté serveur et ne doit pas recevoir ces arguments de ligne de commande.
MM_KWARGS=""
if [ "${VLM_BACKEND}" = "nim_cosmos" ]; then
  case "$VLM_MODEL" in
    *cosmos-reason2*) MM_KWARGS=", \"mm_processor_kwargs\": {\"size\": {\"shortest_edge\": ${MIN_PIXELS}, \"longest_edge\": ${MAX_PIXELS}}}, \"media_io_kwargs\": {\"video\": {\"num_frames\": ${NUM_FRAMES}}}" ;;
    *cosmos*)         MM_KWARGS=", \"mm_processor_kwargs\": {\"videos_kwargs\": {\"min_pixels\": ${MIN_PIXELS}, \"max_pixels\" : ${MAX_PIXELS}}}, \"media_io_kwargs\" : {\"video\" : {\"num_frames\" : ${NUM_FRAMES}}}" ;;
    *)                      MM_KWARGS="" ;;
  esac
fi

curl -s --connect-timeout 5 --max-time 120 -X POST "${VLM_ENDPOINT}/chat/completions" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r '.choices[0].message.content'
{
  "model": $(jq -Rs . <<< "${VLM_MODEL}"),
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": $(jq -Rs . <<< "${PROMPT}")},
        {"type": "video_url", "video_url": {"url": $(jq -Rs . <<< "${VIDEO_URL}")}}
      ]
    }
  ],
  "max_tokens" : 1024,
  "temperature" : 0,0${MM_KWARGS}
}
EOF

Le bloc kwargs tient compte du backend : sur nim_cosmos, les variantes Reason2 (nvidia/cosmos-reason2*) utilisent mm_processor_kwargs.size{shortest_edge,longest_edge} et les autres variantes NIM Cosmos (nvidia/cosmos*) utilisent mm_processor_kwargs.videos_kwargs{min_pixels,max_pixels}; les deux envoient également media_io_kwargs.video.num_frames. Sur rtvlm, aucun kwargs Cosmos n’est envoyé.

Si le VLM renvoie un … bloc (mode de raisonnement Cosmos Reason), ne conservez que le texte situé après comme corps du rapport.

Étape 4 — Remplir le modèle de rapport d’analyse vidéo

Copiez le fichier assets/video-analysis-report.md, remplissez chaque espace réservé, puis renvoyez le code Markdown généré à l’utilisateur. Ne modifiez pas le fichier source. Avant la génération, vérifiez que BROWSER_CLIP_URL est défini et non vide, puis remplacez par cette valeur exacte dans la ligne « URL du clip ». Ne laissez jamais l’espace réservé dans la sortie, n’incluez jamais les instructions du modèle dans une cellule remplie et n’utilisez jamais l’URL brute HOST_IP:30888.

Mode B — Rapport sur les incidents sur une période donnée

Étape 1 — Déterminer la plage horaire et (facultativement) les capteurs

  • start_time / end_time doivent être au format ISO 8601 UTC (AAAA-MM-JJTHH:MM:SS.sssZ). Convertissez les expressions relatives (« dernière heure », « aujourd’hui ») en fonction de l’horloge actuelle de l’hôte.
  • Si l’utilisateur spécifie un capteur, enregistrez-le sous la forme source + source_type=sensor. Sinon, laissez les deux champs vides pour une requête portant sur tous les capteurs.

Étape 2 — Récupérer les incidents via /vss-query-analytics

Transférez le traitement à /vss-query-analytics (initialisation → tools/call) avec :

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "video_analytics__get_incidents",
    "arguments": {
      "source": "",
      "source_type": "sensor",
      "start_time": "",
      "end_time": "",
      "max_count": 100,
      "includes": ["objectIds", "info"]
    }
  },
  "id": 1
}

Limite en lecture seule (obligatoire) :

  • Le mode B est strictement réservé à la consultation des données d’analyse en lecture seule. Il est interdit d’écrire, d’injecter, de rétroactiver ou de modifier les données Elasticsearch/VA.
  • Exemples interdits : indexation d’incidents synthétiques, réinjection de charges utiles de test dans ES, appel d’API d’écriture/mise à jour/suppression pour « rendre les données disponibles » pour le rapport.
  • S'il n'existe aucun incident pour la plage ou le périmètre demandé, traitez cela comme des résultats vides (voir ci-dessous) ; ne fabriquez pas de données.

Pour chaque incident, conservez : id, sensorId, timestamp, end, category, place.name, info.verdict, info.reasoning, objectIds et l’URL du clip (généralement info.clip_url, clip_url ou tout autre champ de pointeur de clip contenu dans la réponse). Appliquez la réécriture $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT (voir « URL de clip lisible par un navigateur » ci-dessus) à chaque URL de clip avant de la coller dans le rapport — la valeur brute est une URL de type HOST_IP:30888 que le navigateur de l’utilisateur ne peut pas atteindre.

Étape 3 — Remplir le modèle de rapport sur la plage d’incidents

Copiez le fichier assets/incident-range-report.md, puis regroupez les données par capteur (ou par catégorie s’il n’y a pas de périmètre de capteur), comptabilisez les verdicts et répertoriez chaque incident avec l’horodatage, la catégorie, le verdict et le raisonnement. Ne modifiez pas le fichier source. Chaque valeur de clip d’incident doit être une URL réécrite et lisible par un navigateur ; omettez la ligne de clip lorsque l’incident ne comporte pas d’URL de clip. N’incluez jamais les instructions du modèle dans une cellule remplie.

Si get_incidents renvoie zéro résultat, ARRÊTEZ-VOUS et renvoyez exactement une ligne de déclaration de plage vide indiquant la plage et la portée demandées. Ne générez pas le modèle complet de rapport de plage d’incidents, n’inventez pas d’incidents, n’ajoutez pas de données de test et ne basculez pas en mode A.

Gestion des erreurs

  • Si une sonde, une requête curl, un appel VLM ou une requête /vss-query-analytics échoue, arrêtez le workflow et signalez le point de terminaison défaillant, le statut HTTP ou l’erreur de commande, ainsi que la prochaine étape de récupération utile. Ne fabriquez pas de rapport à partir de données partielles ou manquantes.
  • Si la réponse VLM est vide, mal formée ou ne contient qu’un bloc de raisonnement, signalez ce problème de réponse et suggérez de vérifier l’état de préparation du modèle et les journaux avant de réessayer.
  • Si l’URL d’un extrait ne peut pas être réécrite vers l’hôte/port public, omettez-la du rapport généré et indiquez que l’URL lisible par un navigateur n’a pas pu être produite.
  • Pour le mode B, traitez les champs d’incident facultatifs manquants (info.reasoning, objectIds, URL du clip) comme des omissions dans le rapport, mais traitez l’absence d’identifiant, d’horodatage ou de catégorie comme une erreur de qualité des données qui doit être signalée.

Références croisées

  • /vss-manage-video-io-storage — liste des capteurs, chronologies et URL de clip pour l’étape 1 du mode A.
  • /vss-query-analytics — récupération d’incidents (et enrichissement du verdict / du raisonnement) pour le mode B, étape 2.
  • /vss-ask-video — questions-réponses VLM ad hoc sur un clip unique (il ne s’agit pas d’un rapport structuré).
  • /vss-summarize-video — utilisé par le mode A pour générer le corps du résumé lorsque le profil LVS est déployé ; le modèle de rapport (étape 4) est toujours renseigné ici.
Voir sur GitHub
---
name: vss-generate-video-report
description: Generates video analysis reports by routing to a VLM backend for per-clip analysis or an analytics backend for incident-range reports, with deployment profile verification and URL rewriting.
license: Apache-2.0
---

# Report

Generate a video analysis report by routing to one of two backends — **never via** `POST /generate` on the VSS agent.

| Mode | Backend |
|---|---|
| **A. Video clip** | `/vss-manage-video-io-storage` → clip URL → **VLM chat/completions** |
| **B. Incident range** | `/vss-query-analytics` → incident list → narrative report |

If the request is ambiguous (e.g. "report on `<sensor>`" with no time range and no incident wording), default to **Mode A**. Ask only if the user mentions both a sensor and a time range. See **Examples** below for the request phrasings that route to each mode.

---

## Instructions

1. **Pick the mode** — Mode A for a single recorded clip/sensor video, Mode B when the request names a time range or incidents/alerts (match against *Examples*).
2. **Verify the deployment profile** for that mode under *Deployment prerequisite*; hand off to `/vss-deploy-profile` if its probe fails.
3. **Run that mode's numbered steps** — *Mode A* or *Mode B* below.
4. **Rewrite every user-facing clip URL** with the `$VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT` one-liner (*Browser-playable clip URL*) before embedding it in the report.
5. **Return the rendered report markdown** to the user.

Output contract for evaluators:
- Mode A top title MUST be exactly `# Video Analysis Report`.
- Mode B top title MUST be exactly `# Incident Range Report` (never `# Incident Report` or sensor-named variants).
- Mode B MUST include `## Basic Information` with the exact required rows from the template (Report Identifier, Range, Scope, Total Incidents, Confirmed / Rejected / Unverified).

---

## Examples

- "Generate a report for this video" / "report on `<sensor-id>`" → **Mode A**
- "Analyze warehouse_01.mp4" / "create an analysis report on the uploaded video" → **Mode A**
- "Report on incidents from 12:31Z to 12:32Z" → **Mode B**
- "Report on alerts today" / "what incidents happened on `<sensor>` last hour" → **Mode B**
- "Summarize alerts on `<sensor>` between `<t1>` and `<t2>`" → **Mode B**

---

## Negative Triggers

Do **not** use this skill when the request is one of the following:

- Ad-hoc visual Q&A on a clip that do not ask explicitly for a report ("what color is the truck?", "what happens at 00:12?") → use `/vss-ask-video`.
- Archive/semantic similarity retrieval ("find forklifts", "search all videos for tailgating") → use `/vss-search-archive`.
- Read-only incident/metrics lookup without report rendering needs → use `/vss-query-analytics`.
- Deploy/teardown/profile changes ("deploy alerts", "switch profile", "bring up base") → use `/vss-deploy-profile`.
- Real-time alert/rule management requests → use `/vss-manage-alerts`.

Never route reports through VSS-agent `POST /generate`.

---

## Deployment prerequisite

**Mode A** needs the VSS **base** profile (VST + VLM NIM).
**Mode B** needs the VSS **alerts** profile (VA-MCP + Elasticsearch).

Probe:

```bash
# Mode A — VST + VLM reachability
curl -sf --max-time 5 "http://${HOST_IP}:30888/vst/api/v1/sensor/version" >/dev/null

# Mode B — VA-MCP
curl -sf --max-time 5 "http://${HOST_IP}:9901/" >/dev/null
```

If the probe fails, hand off to `/vss-deploy-profile` with `-p base` (Mode A) or `-p alerts` (Mode B). **Always** confirm the deploy with the user first.

---

## Clip URLs: VLM input vs browser report link

VST returns clip URLs using the agent-internal `${HOST_IP}:30888` host:port.
Keep that original URL as `VIDEO_URL` for local / in-cluster VLM frame pulls.
Do **not** rewrite the VLM input URL just to make it browser-playable.

Only create `BROWSER_CLIP_URL` for URLs shown in the rendered report. The
deploy layer exports the browser-facing host:port as `$VSS_PUBLIC_HOST` /
`$VSS_PUBLIC_PORT` (and scheme as `$VSS_PUBLIC_HTTP_PROTOCOL`) in every
profile `.env` — Brev or bare-metal — so the report-link rewrite is:

```bash
: "${VSS_PUBLIC_HOST:?Set VSS_PUBLIC_HOST before rewriting clip URLs}"
: "${VSS_PUBLIC_PORT:?Set VSS_PUBLIC_PORT before rewriting clip URLs}"
VSS_PUBLIC_HTTP_PROTOCOL="${VSS_PUBLIC_HTTP_PROTOCOL:-http}"
BROWSER_CLIP_URL=$(echo "$RAW_URL" | sed -E "s|^https?://[^/]+|${VSS_PUBLIC_HTTP_PROTOCOL}://${VSS_PUBLIC_HOST}:${VSS_PUBLIC_PORT}|")
```

If either required public host value is missing, omit the report-facing clip
link and call out that a browser-playable URL could not be produced; do not
block the local VLM analysis path. Apply the rewrite to **every clip URL
surfaced in the rendered report** (Mode A Step 4 Clip URL row; Mode B
per-incident clip sub-bullet). Leave the VLM `video_url` content block in Mode A
Step 3 on the original internal URL when the VLM is local / in-cluster.

---

## Mode A — Report on a recorded video clip

**If the VSS `lvs` profile is deployed** — `curl -sf --max-time 5 "http://${HOST_IP}:38111/v1/ready"` returns HTTP 200 — run `/vss-summarize-video` to produce the summary, then paste its output into the report template in Step 4 and skip Steps 1–3 (the VLM-direct path). Run Steps 1–3 only when `/v1/ready` is non-200.

### Step 1 — Resolve the clip URL

Hand off to `/vss-manage-video-io-storage` to:

1. List sensors and confirm the named `<sensor-id>` exists (upload first if not).
2. Fetch `/storage/<streamId>/timelines` for the recorded range when the user did not supply `startTime` / `endTime`.
3. Request a clip URL:

   ```bash
   curl -s "http://${HOST_IP}:30888/vst/api/v1/storage/file/<streamId>/url?startTime=<startTime>&endTime=<endTime>&container=mp4&disableAudio=true" | jq -r .videoUrl
   ```

   That gives a direct `mp4` URL that the local / in-cluster VLM can pull frames from. Bind it to `VIDEO_URL` (used by the VLM in Step 3) and set `RAW_URL="$VIDEO_URL"` before applying the report-link rewrite to produce `BROWSER_CLIP_URL` for Step 4 — the user's browser cannot reach `$VIDEO_URL` directly.
   Mode A requires the selected VLM endpoint to be able to fetch `VIDEO_URL`.
   Local NIM/RT-VLM deployments normally can; remote endpoints generally cannot
   fetch `localhost`, private `HOST_IP`, or VST-internal URLs. If the live
   `VLM_ENDPOINT` is remote, surface that reachability requirement instead of
   making a chat request that will fail after `/v1/models` succeeds.

### Step 2 — Resolve VLM endpoint and model

The deploy may serve the VLM through either of two stacks. Both expose an OpenAI-compatible `chat/completions` API — pick whichever is live:

| Backend | Env vars | Typical host endpoint | Picked when |
|---|---|---|---|
| **NIM Cosmos** | `VLM_BASE_URL`, `VLM_NAME`, `VLM_MODE`, `VLM_MODEL_TYPE` | `${VLM_BASE_URL}/v1` (no trailing `/v1` on the env var; the agent appends it) | `VLM_MODEL_TYPE != rtvi` **and** `VLM_MODE` ∈ {`local`, `local_shared`, `remote`} **and** `VLM_BASE_URL` is non-empty |
| **RT-VLM Cosmos** | `RTVI_VLM_BASE_URL`, `RTVI_VLM_MODEL_TO_USE`, `VLM_MODEL_TYPE` | `${RTVI_VLM_BASE_URL}/v1` — if unset, derive from `${HOST_IP}` (`http://${HOST_IP}:8018/v1` for alerts, `http://${HOST_IP}:30082/v1` for base) | `VLM_MODEL_TYPE = rtvi`, or `VLM_MODE=none`, or `VLM_BASE_URL` empty; also the only path for `warehouse` |

Read the live values off the running agent container — do not guess:

```bash
docker exec vss-agent sh -lc '
for k in HOST_IP VLM_MODE VLM_MODEL_TYPE VLM_BASE_URL VLM_NAME RTVI_VLM_BASE_URL RTVI_VLM_MODEL_TO_USE; do
  v="$(printenv "$k")"
  [ -n "$v" ] && printf "%s=%s\n" "$k" "$v"
done
'
```

Do not require `RTVI_VLM_ENDPOINT` from `vss-agent` env; several profiles do not inject it.

Selection rule:

```bash
if [ "${VLM_MODEL_TYPE:-}" = "rtvi" ]; then
  VLM_BACKEND="rtvlm"
  VLM_ENDPOINT="${RTVI_VLM_BASE_URL:+${RTVI_VLM_BASE_URL%/}/v1}"
  [ -z "${VLM_ENDPOINT}" ] && VLM_ENDPOINT="http://${HOST_IP}:8018/v1"   # alerts default
  VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
elif [ -n "${VLM_BASE_URL}" ] && [ "${VLM_MODE}" != "none" ]; then
  VLM_BACKEND="nim_cosmos"
  VLM_ENDPOINT="${VLM_BASE_URL%/}/v1"
  VLM_MODEL="${VLM_NAME}"
else
  VLM_BACKEND="rtvlm"
  VLM_ENDPOINT="${RTVI_VLM_BASE_URL:+${RTVI_VLM_BASE_URL%/}/v1}"
  [ -z "${VLM_ENDPOINT}" ] && VLM_ENDPOINT="http://${HOST_IP}:30082/v1"  # base default
  VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
fi
```

Probe `/v1/models` before sending a chat request to confirm the chosen endpoint is alive and the model is loaded:

```bash
curl -sf --max-time 5 "${VLM_ENDPOINT}/models" | jq -r '.data[].id'
```

If the probe fails or the listed ids don't include `${VLM_MODEL}`, fall back to the other backend (or surface the error — never silently pick a model that isn't on the server).

### Step 3 — Call the VLM directly

Use the OpenAI-compatible `chat/completions` endpoint with a `video_url` content block — the same payload shape **and multimodal settings** `video_understanding` builds in `src/vss_agents/tools/video_understanding.py` (`_build_vlm_messages` + the Cosmos `base_vlm.bind(...)` call).

The frame sampling and visual-token (pixel) budget must mirror the **live** `video_understanding` settings for the active profile. **Send `mm_processor_kwargs` and `media_io_kwargs`** so the direct call uses the same frame sampling and pixel budget as the in-agent `video_understanding` tool — omitting them lets the VLM apply its own defaults, so the output diverges from the agent path.

```bash
PROMPT='Describe in detail what happens in the video, with timestamps (start–end in seconds from clip start) for each segment or event. Cover scenes, objects, people, vehicles, and notable actions.'

# Reasoning is OFF by default — matches the base-profile video_understanding config (`reasoning: false`).
# video_understanding.py uses config.reasoning unless the caller overrides it, so default to non-reasoning.
# Append the Cosmos Reason 2 reasoning suffix ONLY when the user explicitly asks for reasoning
# (drop it for non-cosmos-reason2 VLMs). With reasoning off, the response has no <think> block.
if [ "${REASONING:-false}" = "true" ]; then
PROMPT="${PROMPT}

Answer the question using the following format:

<think>
Your reasoning.
</think>

Write your final answer immediately after the </think> tag."
fi

# If Step 3 is run standalone, derive missing backend from current env/model.
[ -z "${VLM_BACKEND:-}" ] && {
  if [ "${VLM_MODEL_TYPE:-}" = "rtvi" ]; then
    VLM_BACKEND="rtvlm"
  elif [[ "${VLM_MODEL:-}" == nvidia/cosmos* ]]; then
    VLM_BACKEND="nim_cosmos"
  else
    VLM_BACKEND="rtvlm"
  fi
}

# Multimodal settings — resolve from the live agent config file path, not hardcoded candidates.
CFG_JSON=$(
docker exec vss-agent python3 -c '
import json, os, yaml
p = os.getenv("VSS_AGENT_CONFIG_FILE")
if not p:
    raise SystemExit("VSS_AGENT_CONFIG_FILE is not set in vss-agent")
if not os.path.isabs(p):
    p = os.path.join("/vss-agent", p.lstrip("./"))
with open(p, encoding="utf-8") as f:
    cfg = yaml.safe_load(f) or {}
vu = (cfg.get("functions", {}) or {}).get("video_understanding", {}) or {}
print(json.dumps({
    "max_fps": int(vu.get("max_fps", 2)),
    "max_frames": int(vu.get("max_frames", 30)),
    "min_pixels": int(vu.get("min_pixels", 3136)),
    "max_pixels": int(vu.get("max_pixels", 8388608)),
}))
')
)
[ -n "${CFG_JSON}" ] || { echo "Failed to read video_understanding config from vss-agent"; exit 1; }
jq -e . >/dev/null <<< "${CFG_JSON}" || { echo "Invalid config JSON from vss-agent"; exit 1; }
MAX_FPS="$(jq -r '.max_fps' <<< "${CFG_JSON}")"
MAX_FRAMES="$(jq -r '.max_frames' <<< "${CFG_JSON}")"
MIN_PIXELS="$(jq -r '.min_pixels' <<< "${CFG_JSON}")"
MAX_PIXELS="$(jq -r '.max_pixels' <<< "${CFG_JSON}")"

# num_frames = min(int(clip_seconds) * max_fps, max_frames), min 1 — matches video_understanding.py.
# clip_seconds (Step 1 endTime-startTime) may be fractional; truncate to integer seconds — bash $((...))
# is integer-only and errors on "15.0"/"1.5". Default 15s -> caps at MAX_FRAMES.
CLIP_SECONDS=$(awk -v s="${CLIP_SECONDS:-15}" 'BEGIN{printf "%d", s}')
NUM_FRAMES=$(( CLIP_SECONDS * MAX_FPS ))
[ "$NUM_FRAMES" -gt "$MAX_FRAMES" ] && NUM_FRAMES=$MAX_FRAMES
[ "$NUM_FRAMES" -lt 1 ] && NUM_FRAMES=1

# Only apply Cosmos mm/media kwargs on the NIM Cosmos path.
# RT-VLM mode uses its own server-side preprocessing and should not receive these kwargs.
MM_KWARGS=""
if [ "${VLM_BACKEND}" = "nim_cosmos" ]; then
  case "$VLM_MODEL" in
    *cosmos-reason2*) MM_KWARGS=", \"mm_processor_kwargs\": {\"size\": {\"shortest_edge\": ${MIN_PIXELS}, \"longest_edge\": ${MAX_PIXELS}}}, \"media_io_kwargs\": {\"video\": {\"num_frames\": ${NUM_FRAMES}}}" ;;
    *cosmos*)         MM_KWARGS=", \"mm_processor_kwargs\": {\"videos_kwargs\": {\"min_pixels\": ${MIN_PIXELS}, \"max_pixels\": ${MAX_PIXELS}}}, \"media_io_kwargs\": {\"video\": {\"num_frames\": ${NUM_FRAMES}}}" ;;
    *)                      MM_KWARGS="" ;;
  esac
fi

curl -s --connect-timeout 5 --max-time 120 -X POST "${VLM_ENDPOINT}/chat/completions" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r '.choices[0].message.content'
{
  "model": $(jq -Rs . <<< "${VLM_MODEL}"),
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": $(jq -Rs . <<< "${PROMPT}")},
        {"type": "video_url", "video_url": {"url": $(jq -Rs . <<< "${VIDEO_URL}")}}
      ]
    }
  ],
  "max_tokens": 1024,
  "temperature": 0.0${MM_KWARGS}
}
EOF
```

> The kwargs block is backend-aware: on `nim_cosmos`, Reason2 variants (`nvidia/cosmos-reason2*`) use `mm_processor_kwargs.size{shortest_edge,longest_edge}` and other NIM Cosmos variants (`nvidia/cosmos*`) use `mm_processor_kwargs.videos_kwargs{min_pixels,max_pixels}`; both also send `media_io_kwargs.video.num_frames`. On `rtvlm`, no Cosmos kwargs are sent.

If the VLM returns a `<think>…</think>` block (Cosmos Reason reasoning mode), keep only the text after `</think>` as the report body.

### Step 4 — Fill the Video Analysis Report template

Copy [`assets/video-analysis-report.md`](assets/video-analysis-report.md), fill every placeholder, and return the rendered markdown to the user. Keep the source asset unchanged. Before rendering, verify `BROWSER_CLIP_URL` is set and non-empty, then replace `<BROWSER_CLIP_URL>` with that exact value in the `Clip URL` row. Never leave the placeholder in the output, never include template instructions in a filled cell, and never use the raw `HOST_IP:30888` URL.

---

## Mode B — Report on incidents in a time range

### Step 1 — Resolve the time range and (optionally) sensor

- `start_time` / `end_time` must be ISO 8601 UTC (`YYYY-MM-DDTHH:MM:SS.sssZ`). Resolve relative phrases ("last hour", "today") against the current host clock.
- If the user names a sensor, capture it as `source` + `source_type=sensor`. Otherwise leave both unset for an all-sensors query.

### Step 2 — Fetch incidents via `/vss-query-analytics`

Hand off to `/vss-query-analytics` (initialize → `tools/call`) with:

```json
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "video_analytics__get_incidents",
    "arguments": {
      "source": "<sensor-id-or-omit>",
      "source_type": "sensor",
      "start_time": "<ISO>",
      "end_time": "<ISO>",
      "max_count": 100,
      "includes": ["objectIds", "info"]
    }
  },
  "id": 1
}
```

Read-only boundary (mandatory):
- Mode B is strictly read-only analytics retrieval. Never write, seed, backfill, or mutate Elasticsearch/VA data.
- Forbidden examples: indexing synthetic incidents, replaying fixture payloads into ES, calling write/update/delete APIs to "make data available" for the report.
- If no incidents exist for the requested range/scope, handle as empty results (see below); do not fabricate data.

For each incident keep: `id`, `sensorId`, `timestamp`, `end`, `category`, `place.name`, `info.verdict`, `info.reasoning`, `objectIds`, and the clip URL (commonly `info.clip_url`, `clip_url`, or whichever clip-pointer field the response carries). **Apply the `$VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT` rewrite (see *Browser-playable clip URL* above) to every clip URL before pasting it into the report** — the raw value is a `HOST_IP:30888` URL the user's browser cannot reach.

### Step 3 — Fill the Incident Range Report template

Copy [`assets/incident-range-report.md`](assets/incident-range-report.md), then group by sensor (or by category if no sensor scope), tally verdicts, and list each incident with timestamp / category / verdict / reasoning. Keep the source asset unchanged. Every incident clip value must be a rewritten browser-playable URL; omit the clip line when the incident carries no clip URL. Never include template instructions in a filled cell.

If `get_incidents` returns zero results, STOP and return exactly a one-line empty-range statement naming the requested range and scope. Do not render the full Incident Range template, do not invent incidents, do not seed test data, and do not fall back to Mode A.

---

## Error Handling

- If a probe, `curl`, VLM call, or `/vss-query-analytics` request fails, stop the workflow and report the failing endpoint, HTTP status or command error, and the next useful recovery step. Do not fabricate a report from partial or missing data.
- If the VLM response is empty, malformed, or contains only a reasoning block, surface that response problem and suggest checking model readiness/logs before retrying.
- If a clip URL cannot be rewritten to the public host/port, omit it from the rendered report and call out that the browser-playable URL could not be produced.
- For Mode B, treat missing optional incident fields (`info.reasoning`, `objectIds`, clip URL) as omissions in the report, but treat missing `id`, `timestamp`, or `category` as a data-quality error that should be reported.

---

## Cross-Reference

- **`/vss-manage-video-io-storage`** — sensor list, timelines, and clip URL for Mode A Step 1.
- **`/vss-query-analytics`** — incident retrieval (and verdict / reasoning enrichment) for Mode B Step 2.
- **`/vss-ask-video`** — ad-hoc VLM Q&A on a single clip (not a structured report).
- **`/vss-summarize-video`** — used by Mode A to produce the summary body when the `lvs` profile is deployed; the report template (Step 4) is still filled here.

Installer vss-generate-video-report

Téléchargez et décompressez les fichiers de compétences dans votre répertoire .claude/skills/.

Télécharger le ZIP

Clonez le dépôt et copiez les fichiers de compétence dans votre projet.

git clone https://github.com/NVIDIA/skills/tree/main/skills/vss-generate-video-report # Copy SKILL.md to your .claude/skills/ directory

Copier Copier
Configuration rapide: Copiez le dossier de la compétence dans .claude/skills/ Claude détectera automatiquement la compétence et l'utilisera
Dépôt NVIDIA/skills

Compétences similaires

microservices-patterns
Heure mise à jour 29 juin 2026
jpa-patterns
Heure mise à jour 30 juin 2026
fabric-lakehouse
Heure mise à jour 30 juin 2026
prisma-expert
Heure mise à jour 29 juin 2026
OR