вариант

vss-generate-video-report

NVIDIA/skills NVIDIA/skills

Генерирует отчеты по анализу видео, направляя данные в бэкэнд VLM для анализа отдельных клипов или в аналитический бэкэнд для отчетов по диапазону инцидентов, с проверкой профиля развертывания и переадресацией URL-адресов.

...Расширить все
2
Обновлено время 27 сентября 2026 г.

Отчет

Создайте отчет по анализу видео, направив запрос на один из двух бэкендов — ни в коем случае не используйте POST /generate на агенте VSS.

Режим Бэкенд
A. Видеоклип /vss-manage-video-io-storage → URL клипа → чат VLM/завершения
B. Диапазон инцидентов /vss-query-analytics → список инцидентов → описательный отчет

Если запрос неоднозначен (например, «отчет по », без указания временного диапазона и формулировки инцидента), по умолчанию используется режим A. Запрашивать дополнительную информацию следует только в том случае, если пользователь указывает и датчик, и временной диапазон. См. приведенные ниже примеры формулировок запросов, которые направляются в каждый из режимов.

Инструкции

  1. Выберите режим — режим A для отдельного записанного фрагмента или видео с датчика, режим B — если в запросе указан временной диапазон или инциденты/оповещения (сравните с примерами).
  2. Проверьте профиль развертывания для данного режима в разделе «Предпосылки развертывания»; перенаправьте запрос в /vss-deploy-profile, если проверка не прошла.
  3. Выполните пронумерованные шаги для данного режима — «Режим A» или «Режим B» ниже.
  4. Перепишите каждый URL-адрес клипа, предназначенный для пользователей, с использованием однострочной строки $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT (URL-адрес клипа, воспроизводимого в браузере) перед его вставкой в отчет.
  5. Верните пользователюотрендеренный отчет в формате Markdown.

Требования к отчету для оценщиков:

  • Заголовок в верхней части отчета в режиме A ДОЛЖЕН быть именно # Отчет по анализу видео.
  • Заголовок в режиме B ДОЛЖЕН быть именно # Отчет о диапазоне инцидентов (ни в коем случае не # Отчет об инцидентах или варианты с названиями датчиков).
  • Режим B ДОЛЖЕН включать раздел ## «Основная информация» с точными обязательными строками из шаблона («Идентификатор отчета», «Диапазон», «Область действия», «Общее количество инцидентов», «Подтвержденные / Отклоненные / Непроверенные»).

Примеры

  • «Создать отчет по этому видео» / «отчет по » → Режим A
  • «Проанализировать warehouse_01.mp4» / «создать отчет об анализе загруженного видео» → Режим A
  • «Отчет об инцидентах с 12:31Z по 12:32Z» → Режим B
  • «Отчет об оповещениях за сегодня» / «Какие инциденты произошли за последний час» → Режим B
  • «Сводка оповещений за в промежутке и » → Режим B

Отрицательные триггеры

Не используйте этот навык, если запрос относится к одному из следующих случаев:

  • Спонтанные визуальные вопросы и ответы по клипу, в которых явно не запрашивается отчет («какого цвета грузовик?», «что происходит в 00:12?») → используйте /vss-ask-video.
  • Поиск по архиву или семантическому сходству («найти вилочные погрузчики», «найти во всех видео случаи езды слишком близко к впереди идущему транспортному средству») → используйте /vss-search-archive.
  • Просмотр инцидентов/метрик только для чтения без необходимости формирования отчета → используйте /vss-query-analytics.
  • Развертывание/сборка/изменения профиля («развернуть оповещения», «переключить профиль», «запустить базовую конфигурацию») → используйте /vss-deploy-profile.
  • Запросы на управление оповещениями и правилами в режиме реального времени → используйте /vss-manage-alerts.

Ни в коем случае не направляйте отчеты через VSS-agent POST /generate.

Предпосылки развертывания

Для режима A требуется базовый профиль VSS (VST + VLM NIM). Для режима B требуется профиль оповещений VSS (VA-MCP + Elasticsearch).

Проверка:

# Режим A — доступность VST + VLM
curl -sf --max-time 5 "http://${HOST_IP}:30888/vst/api/v1/sensor/version" >/dev/null

# Режим B — VA-MCP
curl -sf --max-time 5 "http://${HOST_IP}:9901/" >/dev/null

Если проверка завершится неудачей, передайте задачу команде /vss-deploy-profile с параметром -p base (режим A) или -p alerts (режим B). Всегда сначала согласовывайте развертывание с пользователем.

URL-адреса клипов: входные данные VLM против ссылки на отчёт в браузере

VST возвращает URL-адреса клипов, используя внутренний для агента формат ${HOST_IP}:30888 (хост:порт). Сохраните этот исходный URL-адрес в качестве VIDEO_URL для локального или внутрикластерного извлечения кадров VLM. Не переписывайте URL-адрес входа VLM только для того, чтобы его можно было воспроизвести в браузере.

Создавайте BROWSER_CLIP_URL только для URL-адресов, отображаемых в сгенерированном отчёте. Уровень развёртки экспортирует доступный для браузера хост:порт как $VSS_PUBLIC_HOST / $VSS_PUBLIC_PORT (а схему — как $VSS_PUBLIC_HTTP_PROTOCOL) в каждом файле .env профиля — Brev или bare-metal — поэтому перезапись ссылки в отчете выглядит так:

: "${VSS_PUBLIC_HOST:?Установите VSS_PUBLIC_HOST перед перезаписью URL-адресов клипов}"
: "${VSS_PUBLIC_PORT:?Установите VSS_PUBLIC_PORT перед перезаписью URL-адресов клипов}"
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}|")

Если отсутствует любое из требуемых значений общедоступного хоста, опустите ссылку на клип в отчёте и укажите, что URL-адрес для воспроизведения в браузере не может быть сгенерирован; не блокируйте локальный путь анализа VLM. Примените перезапись ко всем URL-адресам клипов, отображаемым в сгенерированном отчете (режим A, шаг 4, строка «URL клипа»; режим B, подпункт «клип по инциденту»). Оставьте блок содержимого VLM video_url в режиме A, шаг 3, с исходным внутренним URL-адресом, если VLM является локальным или находится внутри кластера.

Режим A — Отчет по записанному видеоклипу

Если развернут профиль VSS lvs — команда curl -sf --max-time 5 "http://${HOST_IP}:38111/v1/ready" возвращает HTTP 200 — запустите /vss-summarize-video для создания сводки, затем вставьте его вывод в шаблон отчета на шаге 4 и пропустите шаги 1–3 (путь напрямую через VLM). Выполняйте шаги 1–3 только в том случае, если /v1/ready возвращает код не 200.

Шаг 1 — Определение URL-адреса клипа

Передайте задачу команде /vss-manage-video-io-storage для:

  1. перечислить датчики и подтвердить наличие указанного существует (если нет — сначала загрузите его).

  2. Получить /storage//timelines для диапазона записи, если пользователь не указал startTime и endTime.

  3. Запросить URL-адрес клипа:

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

    В результате получается прямой URL-адрес файла mp4, из которого локальный или внутрикластерный VLM может извлекать кадры. Свяжите его с VIDEO_URL (используемым VLM на шаге 3) и установите RAW_URL="$VIDEO_URL" перед применением перезаписи ссылки отчета для формирования BROWSER_CLIP_URL для шага 4 — браузер пользователя не может напрямую получить доступ к $VIDEO_URL. Режим A требует, чтобы выбранный конечный узел VLM мог получить доступ к VIDEO_URL. Локальные развертывания NIM/RT-VLM обычно могут это делать; удалённые конечные узлы, как правило, не могут получить доступ к localhost, частному HOST_IP или внутренним URL VST. Если активная конечная точкаVLM_ENDPOINT является удалённой, следует выявить это требование доступности вместо того, чтобы отправлять запрос в чат, который завершится сбоем после успешного выполнения запроса /v1/models.

Шаг 2 — Определение конечной точки VLM и модели

Развертывание может обслуживать VLM через любой из двух стеков. Оба предоставляют API чата/завершения, совместимый с OpenAI — выберите тот, который работает:

Бэкенд Переменные среды Типичный конечный адрес хоста Выбирается, когда
NIM Cosmos VLM_BASE_URL, VLM_NAME, VLM_MODE, VLM_MODEL_TYPE ${VLM_BASE_URL}/v1 (в переменной среды нет конечного /v1; агент добавляет его) VLM_MODEL_TYPE != rtvi и VLM_MODE ∈ {local, local_shared, remote} и VLM_BASE_URL не пуст
RT-VLM Cosmos RTVI_VLM_BASE_URL, RTVI_VLM_MODEL_TO_USE, VLM_MODEL_TYPE ${RTVI_VLM_BASE_URL}/v1 — если не задано, берется из ${HOST_IP} (http://${HOST_IP}:8018/v1 для оповещений, http://${HOST_IP}:30082/v1 для базовых данных) VLM_MODEL_TYPE = rtvi, или VLM_MODE=none, или VLM_BASE_URL пусто; также единственный путь к хранилищу

Считывайте текущие значения из работающего контейнера агента — не гадайте:

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
'

Не требуйте RTVI_VLM_ENDPOINT из переменных окружения vss-agent; некоторые профили не добавляют его.

Правило выбора:

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"   # оповещения по умолчанию
  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"  # базовое значение по умолчанию
  VLM_MODEL="${RTVI_VLM_MODEL_TO_USE}"
fi

Перед отправкой запроса в чат проверьте /v1/models, чтобы убедиться, что выбранная конечная точка работает и модель загружена:

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

Если проверка завершилась неудачно или в списке идентификаторов отсутствует ${VLM_MODEL}, переключитесь на другой бэкенд (или выведите сообщение об ошибке — никогда не выбирайте модель, которой нет на сервере, без уведомления пользователя).

Шаг 3 — Прямой вызов VLM

Используйте конечную точку чата/автозаполнения, совместимую с OpenAI, с блоком содержимого video_url — с той же структурой полезных данных и мультимодальными настройками, которые использует модуль video_understanding в фай ле src/vss_agents/tools/video_understanding.py (_build_vlm_messages + вызов base_vlm.bind(...) из Cosmos).

Дискретизация кадров и бюджет визуальных токенов (пикселей) должны соответствовать текущим настройкам video_understanding для активного профиля. Отправьте mm_processor_kwargs и media_io_kwargs, чтобы прямой вызов использовал те же параметры выборки кадров и бюджет пикселей, что и инструмент video_understanding внутри агента — их пропуск позволяет VLM применять собственные значения по умолчанию, в результате чего выходные данные будут отличаться от результатов, полученных по пути агента.

PROMPT='Подробно опишите, что происходит на видео, с указанием временных меток (начало–конец в секундах от начала клипа) для каждого сегмента или события. Опишите сцены, объекты, людей, транспортные средства и заметные действия.'

# По умолчанию функция вывода обоснований отключена — соответствует конфигурации `video_understanding` базового профиля (`reasoning: false`).
# Скрипт `video_understanding.py` использует значение `config.reasoning`, если вызывающая сторона не переопределяет его, поэтому по умолчанию функция вывода обоснований отключена.
# Добавляйте суффикс обоснования Cosmos Reason 2 ТОЛЬКО в том случае, если пользователь явно запрашивает обоснование
# (исключите его для VLM, не использующих Cosmos Reason 2). При отключенном обосновании в ответе отсутствует  блок .
if [ "${REASONING:-false}" = "true" ]; then
PROMPT="${PROMPT}

Ответьте на вопрос, используя следующий формат:


Ваше обоснование.


Напишите окончательный ответ сразу после тега ."
fi

# Если шаг 3 запускается автономно, определите отсутствующий бэкенд на основе текущей среды/модели.
[ -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
}

# Настройки мультимодальности — определяются по пути к файлу конфигурации агента vss-agent, а не по жестко заданным вариантам.
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 не установлен в 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 "Не удалось прочитать конфигурацию video_understanding из vss-agent"; exit 1; }
jq -e . >/dev/null <<< "${CFG_JSON}" || { echo "Недопустимый JSON-файл конфигурации от 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 — соответствует video_understanding.py.
# clip_seconds (Step 1 endTime-startTime) может содержать дробную часть; округляем до целых секунд — bash $((...))
# поддерживает только целые числа и выдает ошибку при "15.0"/"1.5". По умолчанию 15 с -> ограничение по 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

# Применять аргументы Cosmos mm/media только в пути NIM Cosmos.
# Режим RT-VLM использует собственную серверную предварительную обработку и не должен получать эти аргументы.
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

Блок kwargs зависит от бэкенда: на nim_cosmos варианты Reason2 (nvidia/cosmos-reason2*) используют mm_processor_kwargs.size{shortest_edge,longest_edge}, а другие варианты NIM Cosmos (nvidia/cosmos*) — mm_processor_kwargs.videos_kwargs{min_pixels,max_pixels}; оба также отправляют media_io_kwargs.video.num_frames. На rtvlm аргументы Cosmos не отправляются.

Если VLM возвращает … блок (режим вывода заключений Cosmos Reason), сохраните в качестве тела отчёта только текст после в качестве тела отчёта.

Шаг 4 — Заполнение шаблона отчёта по анализу видео

Скопируйте файл assets/video-analysis-report.md, заполните все заполнители и верните пользователю отрендеренный текст в формате Markdown. Оставьте исходный файл без изменений. Перед рендерингом убедитесь, что переменная BROWSER_CLIP_URL задана и не пуста, затем замените этим точным значением в строке «URL клипа ». Никогда не оставляйте заполнитель в выходных данных, никогда не включайте инструкции шаблона в заполненную ячейку и никогда не используйте исходный URL HOST_IP:30888.

Режим B — Отчет об инцидентах за определенный период времени

Шаг 1 — Определите временной диапазон и (опционально) датчик

  • start_time / end_time должны быть в формате ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ). Преобразуйте относительные выражения («последний час», «сегодня») с учётом текущего времени хоста.
  • Если пользователь указывает датчик, зафиксируйте его как source + source_type=sensor. В противном случае оставьте оба параметра не заданными для запроса по всем датчикам.

Шаг 2 — Получение инцидентов через /vss-query-analytics

Передайте запрос в /vss-query-analytics (инициализация → tools/call) со следующими параметрами:

{
  "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
}

Граница «только для чтения» (обязательно):

  • Режим B — это строго аналитический доступ только для чтения. Ни в коем случае не записывайте, не заполняйте, не дополняйте и не изменяйте данные Elasticsearch/VA.
  • Запрещённые действия: индексирование синтетических инцидентов, повторная отправка тестовых данных в ES, вызов API-интерфейсов записи/обновления/удаления с целью «обеспечения доступности данных» для отчёта.
  • Если для запрошенного диапазона/области не существует инцидентов, обрабатывайте как пустые результаты (см. ниже); не фабрикуйте данные.

Для каждого инцидента сохраняйте: id, sensorId, timestamp, end, category, place.name, info.verdict, info.reasoning, objectIds и URL клипа (обычно info.clip_url, clip_url или любое другое поле-указатель на клип, которое содержится в ответе). Применяйте переадресацию $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT (см. раздел «URL-адрес клипа, воспроизводимого в браузере» выше) к каждому URL-адресу клипа перед вставкой его в отчет — исходное значение представляет собой URL-адрес HOST_IP:30888, к которому браузер пользователя не может получить доступ.

Шаг 3 — Заполнение шаблона отчета об диапазоне инцидентов

Скопируйте файл assets/incident-range-report.md, затем сгруппируйте данные по датчикам (или по категориям, если область действия датчика не указана), подсчитайте результаты проверки и перечислите каждый инцидент с указанием временной метки, категории, результата проверки и обоснования. Не вносите изменения в исходный файл. Каждое значение клипа инцидента должно представлять собой переписанный URL-адрес, поддерживаемый браузером; опускайте строку с клипом, если инцидент не содержит URL-адреса клипа. Ни в коем случае не включайте инструкции шаблона в заполненную ячейку.

Если get_incidents возвращает ноль результатов, ОСТАНОВИТЕСЬ и верните ровно однострочное пустое заявление о диапазоне, в котором указаны запрошенный диапазон и область действия. Не отображайте полный шаблон «Диапазон инцидентов», не придумывайте инциденты, не вводите тестовые данные и не переходите в режим A.

Обработка ошибок

  • Если проба, запрос curl, вызов VLM или запрос /vss-query-analytics завершается сбоем, остановите рабочий процесс и сообщите о неработающем конечной точке, статусе HTTP или ошибке команды, а также о следующем полезном шаге по восстановлению. Не формируйте отчет на основе неполных или отсутствующих данных.
  • Если ответ VLM пустой, имеет некорректный формат или содержит только блок обоснования, сообщите об этой проблеме с ответом и предложите проверить готовность модели/журналы перед повторной попыткой.
  • Если URL-адрес клипа невозможно переписать на общедоступный хост/порт, исключите его из сгенерированного отчета и укажите, что URL-адрес для воспроизведения в браузере не удалось сгенерировать.
  • Для режима B рассматривайте отсутствие необязательных полей инцидента (info.reasoning, objectIds, URL-адрес клипа) как пропуски в отчете, но рассматривайте отсутствие идентификатора, временной метки или категории как ошибку качества данных, о которой следует сообщать.

Перекрёстные ссылки

  • /vss-manage-video-io-storage — список датчиков, временные шкалы и URL-адреса клипов для шага 1 режима A.
  • /vss-query-analytics — извлечение инцидентов (и дополнение вердикта / обоснования) для режима B, шаг 2.
  • /vss-ask-video — оперативный запрос и ответ VLM по отдельному клипу (не структурированный отчет).
  • /vss-summarize-video — используется режимом A для формирования основного текста сводки при развертывании профиля lvs; шаблон отчета (шаг 4) по-прежнему заполняется здесь.
Посмотреть на 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.

Установить vss-generate-video-report

Скачайте файлы навыков и распакуйте их в каталог .claude/skills/.

Скачать ZIP

Клонируйте репозиторий и скопируйте файлы навыка в свой проект.

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

Копировать Копировать
Быстрая настройка: Скопируйте папку со скиллом в каталог .claude/skills/ Claude автоматически обнаружит и запустит этот скилл
Репозиторий NVIDIA/skills

Похожие навыки

microservices-patterns
Обновлено время 29 июня 2026 г.
jpa-patterns
Обновлено время 30 июня 2026 г.
fabric-lakehouse
Обновлено время 30 июня 2026 г.
prisma-expert
Обновлено время 29 июня 2026 г.
OR