vss-generate-video-report
NVIDIA/skills
Genera informes de análisis de vídeo enviándolos a un backend de VLM para el análisis por clip o a un backend de análisis para informes de incidentes, con verificación del perfil de implementación y reescritura de URL.
...Expandir todoInforme
Genera un informe de análisis de vídeo redirigiéndolo a uno de los dos backends; nunca a través de POST /generate en el agente VSS.
| Modo | Servidor |
|---|---|
| A. Clip de vídeo | /vss-manage-video-io-storage → URL del clip → chat/completaciones de VLM |
| B. Rango de incidentes | /vss-query-analytics → lista de incidentes → informe narrativo |
Si la solicitud es ambigua (p. ej., «informe sobre » sin intervalo de tiempo ni descripción del incidente), se utilizará por defecto el Modo A. Pregunta solo si el usuario menciona tanto un sensor como un intervalo de tiempo. Consulta los ejemplos a continuación para ver las formulaciones de las solicitudes que se dirigen a cada modo.
Instrucciones
- Elige el modo: el modo A para un único clip grabado o vídeo de un sensor; el modo B cuando la solicitud especifique un intervalo de tiempo o incidentes/alertas (compáralo con los ejemplos).
- Verifica el perfil de implementación para ese modo en «Requisitos previos de implementación»; transfiere la tarea a
/vss-deploy-profilesi su prueba falla. - Ejecuta los pasos numerados de ese modo: el modo A o el modo B que se indican a continuación.
- Reescribe todas las URL de clips destinadas a los usuarios con la cadena de una sola línea
$VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT(URL de clip reproducible en el navegador) antes de incrustarlas en el informe. - Devuelve al usuarioel informe renderizado en formato Markdown.
Especificaciones del contrato para los evaluadores:
- El título principal del Modo A DEBE ser exactamente
# Informe de análisis de vídeo. - El título principal del Modo B DEBE ser exactamente
# Informe de rango de incidentes(nunca# Informe de incidentesni variantes con el nombre del sensor). - El modo B DEBE incluir
## Información básicacon las filas exactas requeridas de la plantilla (Identificador del informe, Rango, Alcance, Total de incidentes, Confirmados / Rechazados / No verificados).
Ejemplos
- «Generar un informe para este vídeo» / «informe sobre
» → Modo A - «Analizar warehouse_01.mp4» / «crear un informe de análisis sobre el vídeo subido» → Modo A
- «Informe sobre los incidentes desde las 12:31Z hasta las 12:32Z» → Modo B
- «Informe sobre las alertas de hoy» / «¿Qué incidentes han ocurrido en
última hora» → Modo B - «Resumen de alertas de
entrey» → Modo B
Desencadenantes negativos
No utilices esta habilidad cuando la solicitud sea una de las siguientes:
- Preguntas y respuestas visuales puntuales sobre un clip en las que no se solicite explícitamente un informe («¿de qué color es el camión?», «¿qué ocurre en el minuto 00:12?») → utiliza
/vss-ask-video. - Búsqueda en el archivo o por similitud semántica («buscar carretillas elevadoras», «buscar en todos los vídeos casos de conducción demasiado cerca del vehículo de delante») → utiliza
/vss-search-archive. - Consulta de incidentes o métricas en modo de solo lectura sin necesidad de generar un informe → utiliza
/vss-query-analytics. - Implementación, desactivación o cambios de perfil («implementar alertas», «cambiar de perfil», «activar la configuración básica») → utiliza
/vss-deploy-profile. - Solicitudes de gestión de alertas o reglas en tiempo real → utiliza
/vss-manage-alerts.
Nunca envíe informes a través de VSS-agent POST /generate.
Requisitos previos para la implementación
El modo A requiere el perfil base de VSS (VST + VLM NIM). El modo B requiere el perfil de alertas de VSS (VA-MCP + Elasticsearch).
Prueba:
# Modo A — Accesibilidad de VST + VLM
curl -sf --max-time 5 "http://${HOST_IP}:30888/vst/api/v1/sensor/version" >/dev/null
# Modo B — VA-MCP
curl -sf --max-time 5 "http://${HOST_IP}:9901/" >/dev/null
Si la prueba falla, pasa el control a /vss-deploy-profile con -p base (modo A) o -p alerts (modo B). Confirma siempre primero la implementación con el usuario.
URL de clips: entrada de VLM frente al enlace del informe del navegador
VST devuelve las URL de recorte utilizando el host:puerto interno del agente ${HOST_IP}:30888.
Conserva esa URL original como VIDEO_URL para la obtención de fotogramas de VLM locales o dentro del clúster.
No reescribas la URL de entrada de VLM solo para que se pueda reproducir en el navegador.
Crea BROWSER_CLIP_URL únicamente para las URL que aparecen en el informe generado. La
capa de implementación exporta el host:puerto visible para el navegador como $VSS_PUBLIC_HOST /
$VSS_PUBLIC_PORT (y el esquema como $VSS_PUBLIC_HTTP_PROTOCOL) en cada
archivo .env de perfil —ya sea Brev o bare-metal—, por lo que la reescritura del enlace del informe es:
: "${VSS_PUBLIC_HOST:?Establece VSS_PUBLIC_HOST antes de reescribir las URL de los clips}"
: "${VSS_PUBLIC_PORT:?Establece VSS_PUBLIC_PORT antes de reescribir las URL de los clips}"
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 falta alguno de los valores de host público requeridos, omite el enlace del clip
destinado al informe e indica que no se ha podido generar una URL reproducible en el navegador; no
bloquees la ruta de análisis local de VLM. Aplica la reescritura a todas las URL de fragmentos
que aparezcan en el informe generado (Modo A, paso 4, fila «URL del fragmento»; Modo B,
subpunto de fragmentos por incidente). Deja el bloque de contenido video_url del VLM en el Modo A,
paso 3, con la URL interna original cuando el VLM sea local o esté dentro del clúster.
Modo A — Informe sobre un clip de vídeo grabado
Si se ha implementado el perfil VSS lvs — curl -sf --max-time 5 "http://${HOST_IP}:38111/v1/ready" devuelve un código HTTP 200 — ejecute /vss-summarize-video para generar el resumen, a continuación, pega el resultado en la plantilla de informe del paso 4 y omite los pasos 1 a 3 (la ruta directa al VLM). Ejecuta los pasos 1 a 3 solo cuando /v1/ready no devuelva un código 200.
Paso 1: resolver la URL del clip
Pase el control a /vss-manage-video-io-storage para:
enumerar los sensores y confirmar que el sensor
existe (si no es así, subirlo primero).Obtener
/storage/para el intervalo grabado cuando el usuario no haya proporcionado/timelines startTimeniendTime.Solicitar una URL del clip:
curl -s "http://${HOST_IP}:30888/vst/api/v1/storage/file//url?startTime= &endTime= &container=mp4&disableAudio=true" | jq -r .videoUrl Esto proporciona una URL
mp4directa de la que el VLM local o dentro del clúster puede extraer fotogramas. Asigna esta URL aVIDEO_URL(utilizada por el VLM en el paso 3) y estableceRAW_URL="$VIDEO_URL"antes de aplicar la reescritura del enlace del informe para generarBROWSER_CLIP_URLpara el paso 4; el navegador del usuario no puede acceder directamente a$VIDEO_URL. El modo A requiere que el punto final VLM seleccionado pueda recuperarVIDEO_URL. Las implementaciones locales de NIM/RT-VLM normalmente pueden hacerlo; los puntos finales remotos, por lo general, no pueden recuperarlocalhost,HOST_IPprivada ni URL internas de VST. Si elVLM_ENDPOINTen directo es remoto, muestra ese requisito de accesibilidad en lugar de realizar una solicitud de chat que fallará después de que/v1/modelstenga éxito.
Paso 2 — Resolver el punto final VLM y el modelo
La implementación puede servir el VLM a través de cualquiera de las dos pilas. Ambas exponen una API de chat/autocompletado compatible con OpenAI; elige la que esté activa:
| Backend | Variables de entorno | Punto final de host típico | Se selecciona cuando |
|---|---|---|---|
| NIM Cosmos | VLM_BASE_URL, VLM_NAME, VLM_MODE, VLM_MODEL_TYPE |
${VLM_BASE_URL}/v1 (sin «/v1» al final de la variable de entorno; el agente lo añade) |
VLM_MODEL_TYPE ≠ rtvi y VLM_MODE ∈ {local, local_shared, remote} y VLM_BASE_URL no está vacía |
| RT-VLM Cosmos | RTVI_VLM_BASE_URL, RTVI_VLM_MODEL_TO_USE, VLM_MODEL_TYPE |
${RTVI_VLM_BASE_URL}/v1 — si no está definida, se deduce a partir de ${HOST_IP} (http://${HOST_IP}:8018/v1 para alertas, http://${HOST_IP}:30082/v1 para la base) |
VLM_MODEL_TYPE = rtvi, o VLM_MODE=none, o VLM_BASE_URL vacía; también es la única ruta para el almacén |
Lee los valores en tiempo real del contenedor del agente en ejecución — no hagas suposiciones:
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
'
No es necesario que RTVI_VLM_ENDPOINT esté presente en el entorno de vss-agent; hay varios perfiles que no lo incluyen.
Regla de selección:
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" # alertas por defecto
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" # valor predeterminado
VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
fi
Comprueba /v1/models antes de enviar una solicitud de chat para confirmar que el punto final elegido está activo y que el modelo se ha cargado:
curl -sf --max-time 5 "${VLM_ENDPOINT}/models" | jq -r '.data[].id'
Si la comprobación falla o los ID mostrados no incluyen ${VLM_MODEL}, recurre al otro backend (o muestra el error; nunca elijas en silencio un modelo que no esté en el servidor).
Paso 3 — Llamar directamente al VLM
Utiliza el punto final de chat/completaciones compatible con OpenAI con un bloque de contenido video_url: la misma estructura de carga útil y los mismos ajustes multimodales que video_understanding genera en src/vss_agents/tools/video_understanding.py (_build_vlm_messages + la llamada base_vlm.bind(...) de Cosmos).
El muestreo de fotogramas y el presupuesto de tokens visuales (píxeles) deben coincidir con la configuración de video_understanding en tiempo real para el perfil activo. Envía mm_processor_kwargs y media_io_kwargs para que la llamada directa utilice el mismo muestreo de fotogramas y el mismo presupuesto de píxeles que la herramienta video_understanding integrada en el agente; si se omiten, el VLM aplicará sus propios valores predeterminados, por lo que la salida se desviará de la ruta del agente.
PROMPT='Describe con detalle lo que ocurre en el vídeo, con marcas de tiempo (inicio-fin en segundos desde el inicio del clip) para cada segmento o evento. Incluye escenas, objetos, personas, vehículos y acciones destacadas.'
# El razonamiento está desactivado por defecto — coincide con la configuración de video_understanding del perfil base (`reasoning: false`).
# video_understanding.py utiliza config.reasoning a menos que quien lo invoque lo anule, por lo que por defecto no se aplica el razonamiento.
# Añade el sufijo de razonamiento «Cosmos Reason 2» ÚNICAMENTE cuando el usuario solicite explícitamente el razonamiento
# (omítelo para los VLM que no sean «cosmos-reason2»). Con el razonamiento desactivado, la respuesta no incluye el bloque .
if [ "${REASONING:-false}" = "true" ]; then
PROMPT="${PROMPT}
Responde a la pregunta utilizando el siguiente formato:
Tu razonamiento.
Escribe tu respuesta final inmediatamente después de la etiqueta ."
fi
# Si el Paso 3 se ejecuta de forma independiente, deduce el backend que falte a partir del entorno/modelo actual.
[ -z "${VLM_BACKEND:-}" ] && {
if [ "${VLM_BACKEND:-}" = "rtvi" ]; then
VLM_BACKEND="rtvlm"
elif [[ "${VLM_MODEL:-}" == nvidia/cosmos* ]]; then
VLM_BACKEND="nim_cosmos"
else
VLM_BACKEND="rtvlm"
fi
}
# Configuración multimodal: se determina a partir de la ruta del archivo de configuración del agente en tiempo real, no a partir de opciones fijas.
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 no está definido en 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 "Error al leer la configuración de video_understanding desde vss-agent"; exit 1; }
jq -e . >/dev/null <<< "${CFG_JSON}" || { echo "Configuración JSON no válida 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), mínimo 1 — coincide con video_understanding.py.
# clip_seconds (Step 1 endTime-startTime) puede ser un valor fraccionario; se redondea a segundos enteros — bash $((...))
# solo admite números enteros y da error con «15.0»/«1.5». Por defecto, 15 s -> se limita a 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
# Aplicar los argumentos de línea de comandos de Cosmos mm/media únicamente en la ruta NIM de Cosmos.
# El modo RT-VLM utiliza su propio preprocesamiento del lado del servidor y no debe recibir estos argumentos de línea de comandos.
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
El bloque kwargs se adapta al backend: en
nim_cosmos, las variantes de Reason2 (nvidia/cosmos-reason2*) utilizanmm_processor_kwargs.size{shortest_edge,longest_edge}, mientras que otras variantes de NIM Cosmos (nvidia/cosmos*) utilizanmm_processor_kwargs.videos_kwargs{min_pixels,max_pixels}; ambos envían tambiénmedia_io_kwargs.video.num_frames. Enrtvlm, no se envían kwargs de Cosmos.
Si el VLM devuelve un (modo de razonamiento Cosmos Reason), conserva solo el texto que aparece después de como cuerpo del informe.
Paso 4 — Rellena la plantilla del informe de análisis de vídeo
Copia el archivo assets/video-analysis-report.md, rellena todos los marcadores de posición y devuelve el código Markdown generado al usuario. Mantén el archivo original sin cambios. Antes de generar el código, comprueba que BROWSER_CLIP_URL esté definido y no esté vacío; a continuación, sustituye por ese valor exacto en la fila «URL del clip ». Nunca dejes el marcador de posición en la salida, nunca incluyas instrucciones de la plantilla en una celda rellenada y nunca utilices la URL sin procesar HOST_IP:30888.
Modo B — Informe sobre incidentes en un intervalo de tiempo
Paso 1 — Resolver el intervalo de tiempo y (opcionalmente) el sensor
start_time/end_timedeben estar en formato ISO 8601 UTC (AAAA-MM-DDTHH:MM:SS.sssZ). Resuelve las expresiones relativas («última hora», «hoy») en función del reloj actual del host.- Si el usuario especifica un sensor, regístralo como
source+source_type=sensor. De lo contrario, deja ambos campos sin definir para realizar una consulta que abarque todos los sensores.
Paso 2 — Obtener incidentes mediante /vss-query-analytics
Pasa el control a /vss-query-analytics (inicializar → tools/call) con:
{
"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
}
Límite de solo lectura (obligatorio):
- El modo B consiste exclusivamente en la recuperación de datos analíticos de solo lectura. Nunca se deben escribir, inicializar, rellenar ni modificar datos de Elasticsearch/VA.
- Ejemplos prohibidos: indexar incidentes sintéticos, reproducir cargas útiles de pruebas en ES, llamar a las API de escritura/actualización/eliminación para «poner los datos a disposición» del informe.
- Si no existen incidentes para el rango o ámbito solicitado, trátalos como resultados vacíos (véase más abajo); no inventes datos.
Para cada incidente, conserva: id, sensorId, timestamp, end, category, place.name, info.verdict, info.reasoning, objectIds y la URL del clip (normalmente info.clip_url, clip_url o cualquier campo de puntero de clip que contenga la respuesta). Aplica la reescritura $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT (véase «URL de clip reproducible en el navegador» más arriba) a cada URL de clip antes de pegarla en el informe; el valor sin procesar es una URL HOST_IP:30888 a la que el navegador del usuario no puede acceder.
Paso 3 — Rellenar la plantilla del informe de rango de incidentes
Copia el archivo assets/incident-range-report.md; a continuación, agrupa por sensor (o por categoría si no hay un ámbito de sensor), contabiliza los veredictos y enumera cada incidente con la marca de tiempo, la categoría, el veredicto y el razonamiento. Mantén el archivo original sin cambios. Cada valor de clip de incidente debe ser una URL reescrita que se pueda reproducir en el navegador; omite la línea del clip cuando el incidente no contenga ninguna URL de clip. Nunca incluyas instrucciones de la plantilla en una celda rellenada.
Si get_incidents devuelve cero resultados, DETENTE y devuelve exactamente una declaración de rango vacío de una sola línea en la que se indique el rango y el ámbito solicitados. No generes la plantilla completa de «Incident Range», no inventes incidentes, no introduzcas datos de prueba y no recurras al Modo A.
Gestión de errores
- Si falla una sonda, una llamada
curl, una llamada VLM o una solicitud/vss-query-analytics, detén el flujo de trabajo e informa del punto final que ha fallado, del estado HTTP o del error de comando, así como del siguiente paso útil para la recuperación. No elabores un informe a partir de datos parciales o incompletos. - Si la respuesta de VLM está vacía, tiene un formato incorrecto o solo contiene un bloque de razonamiento, señala ese problema en la respuesta y sugiere comprobar la disponibilidad del modelo o los registros antes de volver a intentarlo.
- Si no es posible reescribir la URL de un clip para que apunte al host o puerto público, omítela del informe generado e indica que no se ha podido generar la URL reproducible en el navegador.
- Para el Modo B, trata los campos opcionales del incidente que falten (
info.reasoning,objectIds, URL del clip) como omisiones en el informe, pero trata la faltade ID,marca de tiempoocategoríacomo un error de calidad de los datos que debe notificarse.
Referencia cruzada
/vss-manage-video-io-storage: lista de sensores, líneas de tiempo y URL de clips para el paso 1 del modo A./vss-query-analytics— recuperación de incidentes (y enriquecimiento del veredicto y el razonamiento) para el paso 2 del Modo B./vss-ask-video— Preguntas y respuestas ad hoc de VLM sobre un único clip (no es un informe estructurado)./vss-summarize-video— utilizado por el Modo A para generar el cuerpo del resumen cuando se implementa el perfillvs; la plantilla del informe (Paso 4) se sigue rellenando aquí.
---
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.
Todos los archivos
8 archivosInstalar vss-generate-video-report
Descarga y descomprime los archivos de las habilidades en el directorio .claude/skills/.
Descargar ZIPClona el repositorio y copia los archivos de la habilidad a tu proyecto.
git clone https://github.com/NVIDIA/skills/tree/main/skills/vss-generate-video-report # Copy SKILL.md to your .claude/skills/ directory
Copiar





Hogar
