vss-generate-video-report
NVIDIA/skills
Gera relatórios de análise de vídeo encaminhando-os para um backend VLM para análise por clipe ou para um backend de análise para relatórios de intervalos de incidentes, com verificação do perfil de implantação e reescrita de URLs.
...Expandir tudoRelatório
Gere um relatório de análise de vídeo enviando a solicitação para um dos dois back-ends — nunca por meio do POST /generate no agente VSS.
| Modo | Backend |
|---|---|
| A. Clipe de vídeo | /vss-manage-video-io-storage → URL do clipe → chat/conclusões do VLM |
| B. Intervalo de incidentes | /vss-query-analytics → lista de incidentes → relatório narrativo |
Se a solicitação for ambígua (por exemplo, “relatório sobre " sem intervalo de tempo e sem descrição do incidente), use o Modo A como padrão. Pergunte apenas se o usuário mencionar tanto um sensor quanto um intervalo de tempo. Consulte os exemplos abaixo para ver as formulações de solicitação que direcionam para cada modo.
Instruções
- Escolha o modo — Modo A para um único clipe gravado/vídeo de sensor; Modo B quando a solicitação especificar um intervalo de tempo ou incidentes/alertas (compare com os exemplos).
- Verifique o perfil de implantação para esse modo em “Pré-requisitos de implantação”; encaminhe para
/vss-deploy-profilese a verificação falhar. - Execute as etapas numeradas desse modo — Modo A ou Modo B abaixo.
- Reescreva todas as URLs de clipes voltadas para o usuário com a sequência de uma linha
$VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT(URL de clipe reproduzível no navegador) antes de incorporá-las ao relatório. - Retorne o Markdown do relatório renderizado ao usuário.
Contrato de saída para avaliadores:
- O título principal do Modo A DEVE ser exatamente
# Relatório de Análise de Vídeo. - O título principal do Modo B DEVE ser exatamente
# Relatório de Intervalo de Incidentes(nunca# Relatório de Incidenteou variantes com o nome do sensor). - O Modo B DEVE incluir
## Informações Básicascom as linhas exatas exigidas pelo modelo (Identificador do Relatório, Intervalo, Escopo, Total de Incidentes, Confirmados / Rejeitados / Não Verificados).
Exemplos
- “Gerar um relatório para este vídeo” / “relatório sobre
" → Modo A - “Analise warehouse_01.mp4” / “crie um relatório de análise sobre o vídeo enviado” → Modo A
- “Relatório sobre incidentes das 12h31Z às 12h32Z” → Modo B
- “Relatório sobre os alertas de hoje” / “quais incidentes ocorreram na
última hora” → Modo B - "Resumir os alertas de
entree" → Modo B
Gatilhos negativos
Não use essa habilidade quando a solicitação for uma das seguintes:
- Perguntas e respostas visuais pontuais sobre um clipe que não solicitem explicitamente um relatório (“de que cor é o caminhão?”, “o que acontece aos 00:12?”) → use
/vss-ask-video. - Recuperação de arquivo/semelhança semântica (“encontre empilhadeiras”, “pesquise todos os vídeos por condução muito próxima”) → use
/vss-search-archive. - Consulta de incidentes/métricas somente para leitura, sem necessidade de geração de relatório → use
/vss-query-analytics. - Implantação/desativação/alterações de perfil (“implantar alertas”, “trocar de perfil”, “ativar a configuração básica”) → use
/vss-deploy-profile. - Solicitações de gerenciamento de alertas/regras em tempo real → use
/vss-manage-alerts.
Nunca encaminhe relatórios por meio do POST /generate do VSS-agent.
Pré-requisito de implantação
O Modo A requer o perfil básico do VSS (VST + VLM NIM). O Modo B requer o perfil de alertas do VSS (VA-MCP + Elasticsearch).
Teste:
# Modo A — acessibilidade do 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
Se o teste falhar, encaminhe para /vss-deploy-profile com -p base (Modo A) ou -p alerts (Modo B). Sempre confirme a implantação com o usuário primeiro.
URLs de clipes: entrada do VLM x link do relatório do navegador
O VST retorna URLs de clipes usando o host:porta interno do agente ${HOST_IP}:30888.
Mantenha essa URL original como VIDEO_URL para extrações de quadros do VLM locais ou dentro do cluster.
Não reescreva a URL de entrada do VLM apenas para torná-la reproduzível no navegador.
Crie BROWSER_CLIP_URL apenas para URLs exibidas no relatório renderizado. A
camada de implantação exporta o host:porta voltado para o navegador como $VSS_PUBLIC_HOST /
$VSS_PUBLIC_PORT (e o esquema como $VSS_PUBLIC_HTTP_PROTOCOL) em todos os
arquivos .env de perfil — Brev ou bare-metal —, de modo que a reescrita do link do relatório é:
: "${VSS_PUBLIC_HOST:?Defina VSS_PUBLIC_HOST antes de reescrever URLs de clipes}"
: "${VSS_PUBLIC_PORT:?Defina VSS_PUBLIC_PORT antes de reescrever URLs de clipes}"
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}|")
Se algum dos valores obrigatórios do host público estiver faltando, omita o link do clipe
no relatório e indique que não foi possível gerar uma URL reproduzível no navegador; não
bloqueie o caminho de análise do VLM local. Aplique a reescrita a todas as URLs de clipes
exibidas no relatório gerado (Modalidade A, Etapa 4, linha de URL do clipe; Modalidade B,
subponto de clipe por incidente). Deixe o bloco de conteúdo video_url do VLM na Modalidade A,
Etapa 3, com a URL interna original quando o VLM for local ou estiver no cluster.
Modo A — Relatório sobre um clipe de vídeo gravado
Se o perfil VSS lvs estiver implantado — curl -sf --max-time 5 "http://${HOST_IP}:38111/v1/ready" retorna HTTP 200 — execute /vss-summarize-video para gerar o resumo, em seguida, cole a saída no modelo de relatório na Etapa 4 e pule as Etapas 1–3 (o caminho direto do VLM). Execute as Etapas 1–3 somente quando /v1/ready não retornar 200.
Etapa 1 — Resolver a URL do clipe
Passe para o /vss-manage-video-io-storage para:
Listar sensores e confirmar se o sensor nomeado
(faça o upload primeiro, caso não existam).Buscar
/storage/para o intervalo gravado quando o usuário não forneceu/timelines startTime/endTime.Solicitar uma URL do clipe:
curl -s "http://${HOST_IP}:30888/vst/api/v1/storage/file//url?startTime= &endTime= &container=mp4&disableAudio=true" | jq -r .videoUrl Isso gera uma URL direta
do arquivo MP4da qual o VLM local ou dentro do cluster pode extrair quadros. Associe-a aVIDEO_URL(usada pelo VLM na Etapa 3) e definaRAW_URL="$VIDEO_URL"antes de aplicar a reescrita do link do relatório para gerarBROWSER_CLIP_URLpara a Etapa 4 — o navegador do usuário não consegue acessar$VIDEO_URLdiretamente. O Modo A exige que o endpoint VLM selecionado seja capaz de buscarVIDEO_URL. Implantações locais de NIM/RT-VLM normalmente conseguem; endpoints remotos geralmente não conseguem buscarlocalhost,HOST_IPprivado ou URLs internas do VST. Se oVLM_ENDPOINTativo for remoto, exponha esse requisito de acessibilidade em vez de fazer uma solicitação de chat que falhará após o sucesso de/v1/models.
Etapa 2 — Resolver o endpoint VLM e o modelo
A implantação pode disponibilizar o VLM por meio de uma das duas pilhas. Ambas expõem uma API de bate-papo/completações compatível com a OpenAI — escolha aquela que estiver ativa:
| Backend | Variáveis de ambiente | Endpoint típico do host | Escolhido quando |
|---|---|---|---|
| NIM Cosmos | VLM_BASE_URL, VLM_NAME, VLM_MODE, VLM_MODEL_TYPE |
${VLM_BASE_URL}/v1 (sem /v1 no final da variável de ambiente; o agente o acrescenta) |
VLM_MODEL_TYPE != rtvi e VLM_MODE ∈ {local, local_shared, remote} e VLM_BASE_URL não está vazio |
| RT-VLM Cosmos | RTVI_VLM_BASE_URL, RTVI_VLM_MODEL_TO_USE, VLM_MODEL_TYPE |
${RTVI_VLM_BASE_URL}/v1 — se não estiver definido, derivar de ${HOST_IP} (http://${HOST_IP}:8018/v1 para alertas, http://${HOST_IP}:30082/v1 para a base) |
VLM_MODEL_TYPE = rtvi, ou VLM_MODE=none, ou VLM_BASE_URL vazio; também é o único caminho para o warehouse |
Leia os valores em tempo real do contêiner do agente em execução — não adivinhe:
docker exec vss-agent sh -lc '
para k em HOST_IP VLM_MODE VLM_MODEL_TYPE VLM_BASE_URL VLM_NAME RTVI_VLM_BASE_URL RTVI_VLM_MODEL_TO_USE; faça
v="$(printenv "$k")"
[ -n "$v" ] && printf "%s=%s\n" "$k" "$v"
done
'
Não é necessário que RTVI_VLM_ENDPOINT esteja presente no ambiente do vss-agent; vários perfis não o inserem.
Regra de seleção:
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" # alerta padrão
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" # padrão básico
VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
fi
Verifique /v1/models antes de enviar uma solicitação de chat para confirmar se o endpoint escolhido está ativo e se o modelo está carregado:
curl -sf --max-time 5 "${VLM_ENDPOINT}/models" | jq -r '.data[].id'
Se a verificação falhar ou se os IDs listados não incluírem ${VLM_MODEL}, recorra ao outro backend (ou exiba o erro — nunca escolha silenciosamente um modelo que não esteja no servidor).
Etapa 3 — Chame o VLM diretamente
Use o endpoint de chat/completações compatível com a OpenAI com um bloco de conteúdo video_url — o mesmo formato de carga útil e as mesmas configurações multimodais que o video_understanding constrói em src/vss_agents/tools/video_understanding.py (_build_vlm_messages + a chamada base_vlm.bind(...) do Cosmos).
A amostragem de quadros e o orçamento de tokens visuais (pixels) devem corresponder às configurações do video_understanding em tempo real para o perfil ativo. Envie mm_processor_kwargs e media_io_kwargs para que a chamada direta use a mesma amostragem de quadros e o mesmo orçamento de pixels que a ferramenta video_understanding no agente — omitir esses parâmetros permite que o VLM aplique seus próprios padrões, fazendo com que a saída diverja do caminho do agente.
PROMPT='Descreva em detalhes o que acontece no vídeo, com marcas de tempo (início–fim em segundos a partir do início do clipe) para cada segmento ou evento. Aborde cenas, objetos, pessoas, veículos e ações notáveis.'
# O raciocínio está DESATIVADO por padrão — corresponde à configuração do perfil básico do `video_understanding` (`reasoning: false`).
# O `video_understanding.py` usa `config.reasoning`, a menos que o chamador o substitua; portanto, o padrão é sem raciocínio.
# Acrescente o sufixo de raciocínio Cosmos Reason 2 SOMENTE quando o usuário solicitar explicitamente o raciocínio
# (omita-o para VLMs que não sejam Cosmos Reason 2). Com o raciocínio desativado, a resposta não contém o bloco .
if [ "${REASONING:-false}" = "true" ]; then
PROMPT="${PROMPT}
Responda à pergunta usando o seguinte formato:
Seu raciocínio.
Escreva sua resposta final imediatamente após a tag ."
fi
# Se a Etapa 3 for executada de forma independente, determine o backend ausente a partir do ambiente/modelo atual.
[ -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
}
# Configurações multimodais — determinar a partir do caminho do arquivo de configuração do agente ativo, e não de opções pré-definidas.
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ão está definido no 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", {}) ou {}).get("video_understanding", {}) ou {}
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 "Falha ao ler a configuração do video_understanding do vss-agent"; exit 1; }
jq -e . >/dev/null <<< "${CFG_JSON}" || { echo "JSON de configuração inválido do 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 — corresponde ao video_understanding.py.
# clip_seconds (Step 1 endTime-startTime) pode conter frações; arredondar para segundos inteiros — o bash $((...))
# aceita apenas números inteiros e gera erro com "15,0"/"1,5". Padrão 15 s -> limitado 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
# Aplique os argumentos de linha de comando do Cosmos mm/media apenas no caminho do NIM Cosmos.
# O modo RT-VLM usa seu próprio pré-processamento do lado do servidor e não deve receber esses argumentos de linha de comando.
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
O bloco kwargs é sensível ao backend: no
nim_cosmos, as variantes do Reason2 (nvidia/cosmos-reason2*) usammm_processor_kwargs.size{shortest_edge,longest_edge}e outras variantes do NIM Cosmos (nvidia/cosmos*) usammm_processor_kwargs.videos_kwargs{min_pixels,max_pixels}; ambos também enviammedia_io_kwargs.video.num_frames. Nortvlm, nenhum kwargs do Cosmos é enviado.
Se o VLM retornar um (modo de raciocínio Cosmos Reason), mantenha apenas o texto após como corpo do relatório.
Etapa 4 — Preencha o modelo do Relatório de Análise de Vídeo
Copie o arquivo assets/video-analysis-report.md, preencha todos os espaços reservados e retorne o Markdown renderizado ao usuário. Mantenha o arquivo original inalterado. Antes da renderização, verifique se BROWSER_CLIP_URL está definido e não está vazio; em seguida, substitua por esse valor exato na linha “URL do clipe ”. Nunca deixe o espaço reservado na saída, nunca inclua instruções do modelo em uma célula preenchida e nunca use a URL bruta HOST_IP:30888.
Modo B — Relatório sobre incidentes em um intervalo de tempo
Etapa 1 — Defina o intervalo de tempo e (opcionalmente) o sensor
start_time/end_timedevem estar no formato ISO 8601 UTC (AAAA-MM-DDTHH:MM:SS.sssZ). Resolva expressões relativas (“última hora”, “hoje”) com base no relógio atual do host.- Se o usuário especificar um sensor, registre-o como
source+source_type=sensor. Caso contrário, deixe ambos em branco para uma consulta que abranja todos os sensores.
Etapa 2 — Obter incidentes via /vss-query-analytics
Encaminhe para /vss-query-analytics (inicializar → tools/call) com:
{
"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 de somente leitura (obrigatório):
- O Modo B é estritamente para recuperação de análises somente leitura. Nunca grave, insira dados iniciais, preencha retroativamente ou altere dados do Elasticsearch/VA.
- Exemplos proibidos: indexar incidentes sintéticos, reproduzir cargas úteis de fixtures no ES, chamar APIs de gravação/atualização/exclusão para “disponibilizar dados” para o relatório.
- Se não houver incidentes para o intervalo/escopo solicitado, trate como resultados vazios (veja abaixo); não invente dados.
Para cada incidente, mantenha: id, sensorId, timestamp, end, category, place.name, info.verdict, info.reasoning, objectIds e a URL do clipe (geralmente info.clip_url, clip_url ou qualquer campo de ponteiro de clipe que a resposta contenha). Aplique a reescrita $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT (consulte “URL do clipe reproduzível no navegador” acima) a cada URL de clipe antes de colá-la no relatório — o valor bruto é uma URL do tipo HOST_IP:30888 que o navegador do usuário não consegue acessar.
Etapa 3 — Preencha o modelo de Relatório de Intervalo de Incidentes
Copie o arquivo assets/incident-range-report.md, depois agrupe por sensor (ou por categoria, se não houver escopo de sensor), some os veredictos e liste cada incidente com data e hora / categoria / veredicto / justificativa. Mantenha o arquivo original inalterado. Cada valor de clipe de incidente deve ser uma URL reescrita e reproduzível no navegador; omita a linha do clipe quando o incidente não contiver nenhuma URL de clipe. Nunca inclua instruções do modelo em uma célula preenchida.
Se get_incidents retornar zero resultados, PARE e retorne exatamente uma declaração de intervalo vazio de uma linha, indicando o intervalo e o escopo solicitados. Não renderize o modelo completo de Intervalo de Incidentes, não invente incidentes, não insira dados de teste e não recorra ao Modo A.
Tratamento de erros
- Se uma sonda,
um curl, uma chamada VLM ou uma solicitação/vss-query-analyticsfalhar, interrompa o fluxo de trabalho e relate o endpoint com falha, o status HTTP ou o erro de comando, além da próxima etapa útil de recuperação. Não crie um relatório a partir de dados parciais ou ausentes. - Se a resposta do VLM estiver vazia, malformada ou contiver apenas um bloco de justificativa, destaque esse problema na resposta e sugira a verificação da prontidão do modelo e dos logs antes de tentar novamente.
- Se uma URL de clipe não puder ser reescrita para o host/porta pública, omita-a do relatório gerado e indique que não foi possível produzir a URL reproduzível no navegador.
- Para o Modo B, trate os campos opcionais de incidente ausentes (
info.reasoning,objectIds, URL do clipe) como omissões no relatório, mas trate a ausênciade id,carimbo de data/horaoucategoriacomo um erro de qualidade de dados que deve ser relatado.
Referência cruzada
/vss-manage-video-io-storage— lista de sensores, linhas do tempo e URL do clipe para a Etapa 1 do Modo A./vss-query-analytics— recuperação de incidentes (e enriquecimento de veredicto/raciocínio) para a Etapa 2 do Modo B./vss-ask-video— perguntas e respostas ad hoc do VLM sobre um único clipe (não é um relatório estruturado)./vss-summarize-video— usado pelo Modo A para gerar o corpo do resumo quando o perfillvsé implantado; o modelo de relatório (Etapa 4) ainda é preenchido aqui.
---
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 os arquivos
8 arquivosInstalar vss-generate-video-report
Baixe e descompacte os arquivos de habilidades no diretório .claude/skills/.
Baixar ZIPClone o repositório e copie os arquivos da habilidade para o seu projeto.
git clone https://github.com/NVIDIA/skills/tree/main/skills/vss-generate-video-report # Copy SKILL.md to your .claude/skills/ directory
Copiar





Lar
