vss-generate-video-report について
vss-generate-video-report は、VSS 環境で動画分析レポートを作成するスキルです。依頼に応じて単一クリップの分析か期間内のインシデント取得を選び、結果を Markdown レポートにまとめます。
主な機能
- 01依頼内容に応じたレポート方式の選択
- 02動画クリップ分析バックエンドへの振り分け
- 03期間指定のインシデント照会
- 04Markdown レポートの生成
- 05レポート表示用クリップ URL の変換
活用シーン
- 01録画クリップの分析レポート作成
- 02指定期間のアラートの要約
- 03センサー別インシデントレポートの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ promptingcompany/ nv-skills --skill vss-generate-video-reportインストール後、Claude Code で/vss-generate-video-reportを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
BENCHMARK.mdskill-card.mdskill.oms.sigassets/incident-range-report.mdvideo-analysis-report.mdevals/base_profile_report.jsonevals.json詳細
レポート
2 つのバックエンドのいずれかにルーティングして動画分析レポートを生成します。VSS エージェントの POST /generate は絶対に使用しないでください。
| モード | バックエンド |
|---|---|
| A. 動画クリップ | /vss-manage-video-io-storage → クリップ URL → VLM チャット/completions |
| B. インシデント範囲 | /vss-query-analytics → インシデント一覧 → ナラティブレポート |
リクエストが曖昧な場合(例:「<sensor> のレポート」で、時間範囲もインシデントに関する文言もない場合)は、モード A をデフォルトにします。センサーと時間範囲の両方に言及されている場合にのみ、ユーザーに確認してください。各モードにルーティングされるリクエストの表現については、以下の例を参照してください。
手順
- モードを選択する — 単一の録画クリップ/センサー動画の場合はモード A、リクエストに時間範囲またはインシデント/アラートが含まれる場合はモード B を選択します(例に照らして判断してください)。
- そのモードのデプロイの前提条件を確認します。プローブに失敗した場合は
/vss-deploy-profileに引き継ぎます。 - 該当するモードの番号付き手順を実行します — 以下のモード A またはモード B。
- レポートに埋め込む前に、ユーザーに表示するクリップ URL はすべて
$VSS_PUBLIC_HOST:$VSS_PUBLIC_PORTのワンライナーを使って書き換えます(ブラウザーで再生できるクリップ URL)。 - レンダリング済みのレポート Markdown をユーザーに返します。
評価用の出力契約:
- モード A の最上位タイトルは、必ず
# Video Analysis Reportとしてください。 - モード B の最上位タイトルは、必ず
# Incident Range Reportとしてください(# Incident Reportやセンサー名を含むタイトルは不可)。 - モード B には、テンプレートで必須とされている正確な行(Report Identifier、Range、Scope、Total Incidents、Confirmed / Rejected / Unverified)を含む
## Basic Informationを必ず含めてください。
例
- 「この動画のレポートを生成して」/「
<sensor-id>のレポートを作成して」 → モード A - 「warehouse_01.mp4 を分析して」/「アップロードした動画の分析レポートを作成して」 → モード A
- 「12:31Z から 12:32Z までのインシデントをレポートして」 → モード B
- 「今日のアラートをレポートして」/「過去 1 時間に
<sensor>で発生したインシデントは?」 → モード B - 「
<sensor>で<t1>から<t2>までに発生したアラートを要約して」 → モード B
対象外のリクエスト
リクエストが以下のいずれかに該当する場合は、このスキルを使用しないでください。
- クリップに関するアドホックな視覚的な質疑応答で、明示的にレポートを求めていない場合(「トラックは何色ですか?」「00:12 に何が起きますか?」) →
/vss-ask-videoを使用します。 - アーカイブ/意味的類似性による検索(「フォークリフトを見つけて」「テールゲーティングがある動画をすべて検索して」) →
/vss-search-archiveを使用します。 - レポートのレンダリングを必要としない、読み取り専用のインシデント/メトリクス検索 →
/vss-query-analyticsを使用します。 - デプロイ/ティアダウン/プロファイルの変更(「アラートをデプロイして」「プロファイルを切り替えて」「base を起動して」) →
/vss-deploy-profileを使用します。 - リアルタイムのアラート/ルール管理リクエスト →
/vss-manage-alertsを使用します。
レポートのルーティングに VSS エージェントの POST /generate を使用しないでください。
デプロイの前提条件
モード A には VSS の base プロファイル(VST + VLM NIM)が必要です。 モード B には VSS の alerts プロファイル(VA-MCP + Elasticsearch)が必要です。
プローブ:
# 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
プローブに失敗した場合は、モード A では -p base、モード B では -p alerts を指定して /vss-deploy-profile に引き継ぎます。デプロイを実行する前に、必ずユーザーに確認してください。
クリップ URL:VLM 入力とブラウザー向けレポートリンク
VST は、エージェント内部の ${HOST_IP}:30888 ホストとポートを使用するクリップ URL を返します。
ローカル/クラスター内での VLM フレーム取得には、その元の URL を VIDEO_URL として保持します。
ブラウザーで再生できるようにする目的だけで、VLM 入力 URL を書き換えないでください。
レンダリングされたレポートに表示する URL に限り、BROWSER_CLIP_URL を作成します。
デプロイ層は、Brev とベアメタルのいずれのプロファイルでも、ブラウザー向けのホストとポートを $VSS_PUBLIC_HOST/$VSS_PUBLIC_PORT として、スキームを $VSS_PUBLIC_HTTP_PROTOCOL として、各プロファイルの .env にエクスポートします。そのため、レポートリンクの書き換えは次のとおりです。
: "${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}|")
必要な公開ホストの値がいずれか不足している場合は、レポート向けのクリップリンクを省略し、ブラウザーで再生可能な URL を生成できなかったことを明記してください。ローカル VLM 分析の経路は妨げないでください。レンダリングされたレポートに表示するすべてのクリップ URLに書き換えを適用してください(Mode A の Step 4 の Clip URL 行、Mode B の各インシデントのクリップ副項目)。VLM がローカル/クラスター内にある場合、Mode A の Step 3 にある VLM の video_url コンテンツブロックは、元の内部 URL のままにしてください。
Mode A — 録画された動画クリップのレポート
VSS の lvs プロファイルがデプロイされている場合 — curl -sf --max-time 5 "http://${HOST_IP}:38111/v1/ready" が HTTP 200 を返す場合 — /vss-summarize-video を実行して要約を生成し、その出力を Step 4 のレポートテンプレートに貼り付けて、Step 1~3(VLM 直接利用の経路)は省略します。Step 1~3 は /v1/ready が 200 以外の場合にのみ実行してください。
Step 1 — クリップ URL を解決する
/vss-manage-video-io-storage に引き継ぎ、次の作業を行います。
-
センサーを一覧表示し、指定された
<sensor-id>が存在することを確認します(まだアップロードされていない場合は、先にアップロードします)。 -
ユーザーから
startTime/endTimeが指定されていない場合は、録画範囲の/storage/<streamId>/timelinesを取得します。 -
クリップ URL をリクエストします。
curl -s "http://${HOST_IP}:30888/vst/api/v1/storage/file/<streamId>/url?startTime=<startTime>&endTime=<endTime>&container=mp4&disableAudio=true" | jq -r .videoUrl
これにより、ローカル/クラスター内の VLM がフレームを取得できる直接の mp4 URL が得られます。これを VIDEO_URL に割り当て(Step 3 で VLM が使用)、レポートリンクの書き換えを適用する前に RAW_URL="$VIDEO_URL" を設定して、Step 4 用の BROWSER_CLIP_URL を生成します。ユーザーのブラウザーから $VIDEO_URL に直接アクセスすることはできません。
Mode A では、選択した VLM エンドポイントが VIDEO_URL を取得できる必要があります。
ローカルの NIM/RT-VLM デプロイでは通常取得できますが、リモートエンドポイントでは一般に localhost、プライベートな HOST_IP、VST 内部 URL を取得できません。実行中の VLM_ENDPOINT がリモートの場合、/v1/models の成功後に失敗するチャットリクエストを送るのではなく、その到達性要件を明示してください。
Step 2 — VLM エンドポイントとモデルを解決する
デプロイでは、次のいずれか 2 つのスタックを介して VLM を提供できます。どちらも OpenAI 互換の chat/completions API を公開します。稼働中のものを選択してください。
| バックエンド | 環境変数 | 一般的なホストエンドポイント | 選択条件 |
|---|---|---|---|
| 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、base の場合は http://${HOST_IP}:30082/v1) | VLM_MODEL_TYPE = rtvi、または VLM_MODE=none、または VLM_BASE_URL が空の場合。warehouse 用の唯一の経路でもあります |
実行中のエージェントコンテナから実際の値を読み取ってください。推測してはいけません。
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
'
vss-agent の環境変数に RTVI_VLM_ENDPOINT が含まれていることを必須にしないでください。複数のプロファイルでは、この変数は設定されません。
選択ルール:
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
チャットリクエストを送る前に /v1/models を確認し、選択したエンドポイントが稼働中で、モデルが読み込まれていることを確かめてください。
curl -sf --max-time 5 "${VLM_ENDPOINT}/models" | jq -r '.data[].id'
確認に失敗する場合、または一覧の ID に ${VLM_MODEL} が含まれていない場合は、もう一方のバックエンドに切り替えてください(またはエラーを明示してください。サーバーに存在しないモデルを黙って選択してはいけません)。
ステップ 3 — VLM を直接呼び出す
OpenAI 互換の chat/completions エンドポイントを video_url コンテンツブロックとともに使用します。これは src/vss_agents/tools/video_understanding.py 内で video_understanding が構築するものと同じペイロード形状およびマルチモーダル設定です(_build_vlm_messages と Cosmos の base_vlm.bind(...) 呼び出し)。
フレームサンプリングとビジュアルトークン(ピクセル)バジェットは、アクティブなプロファイルに対するライブの video_understanding 設定を反映している必要があります。mm_processor_kwargs と media_io_kwargs を送信して、直接呼び出しがエージェント内の video_understanding ツールと同じフレームサンプリングとピクセルバジェットを使用するようにしてください。これらを省略すると、VLM が独自のデフォルトを適用するため、出力がエージェント経路と乖離します。
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
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 kwargs は送信されません。
VLM が `` ブロック(Cosmos Reason 推論モード)を返す場合は、</think> より後のテキストのみをレポート本文として保持してください。
ステップ 4 — Video Analysis Report テンプレートを記入する
assets/video-analysis-report.md をコピーし、すべてのプレースホルダーを記入して、レンダリングされた Markdown をユーザーに返します。ソースアセットは変更しないでください。レンダリング前に、BROWSER_CLIP_URL が設定されていて空でないことを確認し、Clip URL 行の <BROWSER_CLIP_URL> をその正確な値で置き換えます。出力にプレースホルダーを残さず、記入済みのセルにテンプレートの指示を含めず、生の HOST_IP:30888 URL を使用しないでください。
モード B — 時間範囲内のインシデントに関するレポート
ステップ 1 — 時間範囲と(任意で)センサーを解決する
start_time/end_timeは ISO 8601 UTC(YYYY-MM-DDTHH:MM:SS.sssZ)である必要があります。相対的な表現(「直近 1 時間」「今日」)は、現在のホストクロックに対して解決してください。- ユーザーがセンサーを指定した場合は、
source+source_type=sensorとして取得します。それ以外の場合は、全センサーのクエリのために両方とも未設定のままにします。
ステップ 2 — /vss-query-analytics 経由でインシデントを取得する
以下を指定して /vss-query-analytics(initialize → tools/call)に処理を渡します:
{
"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
}
読み取り専用の境界(必須):
- モード B は厳密に読み取り専用の分析取得です。Elasticsearch/VA データに対して書き込み、シード、バックフィル、または変更を決して行わないでください。
- 禁止例: 合成インシデントのインデックス登録、フィクスチャペイロードの ES へのリプレイ、レポートのために「データを利用可能にする」ための write/update/delete API の呼び出し。
- 要求された範囲/スコープにインシデントが存在しない場合は、空の結果として処理します(以下を参照)。データを捏造しないでください。
各インシデントについて、id、sensorId、timestamp、end、category、place.name、info.verdict、info.reasoning、objectIds、およびクリップ URL(一般に info.clip_url、clip_url、またはレスポンスが持つクリップポインターフィールド)を保持します。レポートに貼り付ける前に、すべてのクリップ URL に $VSS_PUBLIC_HOST:$VSS_PUBLIC_PORT の書き換え(上記の ブラウザで再生可能なクリップ URL を参照)を適用してください — 生の値はユーザーのブラウザから到達できない HOST_IP:30888 URL です。
ステップ 3 — Incident Range Report テンプレートを記入する
assets/incident-range-report.md をコピーし、センサー別(センサースコープがない場合はカテゴリ別)にグループ化し、判定を集計し、各インシデントをタイムスタンプ / カテゴリ / 判定 / 推論とともに列挙します。ソースアセットは変更しないでください。各インシデントのクリップ値は、書き換えられたブラウザで再生可能な URL でなければなりません。インシデントにクリップ URL がない場合は、クリップ行を省略します。記入済みのセルにテンプレートの指示を含めないでください。
get_incidents が 0 件の結果を返す場合は、停止し、要求された範囲とスコープを示す 1 行の空範囲ステートメントを正確に返します。Incident Range テンプレート全体をレンダリングせず、インシデントを捏造せず、テストデータをシードせず、モード A にフォールバックしないでください。
エラー処理
- プローブ、
curl、VLM 呼び出し、または/vss-query-analyticsリクエストが失敗した場合は、ワークフローを停止し、失敗したエンドポイント、HTTP ステータスまたはコマンドエラー、および次に有効な復旧手順を報告してください。不完全または欠落したデータからレポートを捏造しないでください。 - VLM の応答が空、不正な形式、または推論ブロックのみの場合は、その応答の問題を明示し、再試行する前にモデルの準備状態やログを確認するよう提案してください。
- クリップ URL を公開ホスト/ポートに書き換えられない場合は、レンダリングされたレポートからその URL を除外し、ブラウザーで再生可能な URL を生成できなかったことを明記してください。
- モード B では、任意のインシデントフィールド(
info.reasoning、objectIds、クリップ URL)が欠落している場合はレポート上で省略しますが、id、timestamp、またはcategoryが欠落している場合は、データ品質エラーとして報告してください。
相互参照
/vss-manage-video-io-storage— モード A のステップ 1 で使用するセンサー一覧、タイムライン、クリップ URL。/vss-query-analytics— モード B のステップ 2 で使用するインシデント取得(および判定/推論による補足)。/vss-ask-video— 単一のクリップに対するアドホックな VLM の Q&A(構造化レポートではありません)。/vss-summarize-video—lvsプロファイルがデプロイされている場合、モード A で要約本文を作成するために使用します。レポートテンプレート(ステップ 4)はこちらで引き続き入力します。