fix(fam-edge): 整视频上传方式修正 - Gemini 改 resumable 可续传协议(370MB 不再 raw 单发超时);NVIDIA 改 Assets API 上传拿 asset_id(video_url 引用),规避 25MB payload 上限

This commit is contained in:
ericwyuan
2026-08-21 11:37:24 +08:00
parent f20b6c9254
commit d54070ece4
2 changed files with 101 additions and 23 deletions

View File

@@ -5,12 +5,13 @@ provider_name = "nvidia"
模型: nvidia/nemotron-nano-12b-v2-vlNIM 官方支持整视频 video_url 输入,内部自行采样帧)
角色: vision (整视频直出结构化 JSON) + 智能问答
SDK: openai (NIM 兼容 OpenAI API 规范)
整视频分析: 整视频 base64 经 video_url 单次调用 —— 本地不切片、不抽帧
整视频分析: 先经 NVIDIA Assets API 上传完整视频拿 asset_id再以 video_url 引用单次调用
—— 本地不切片、不抽帧request payload 有 25MB 上限base64 直塞不可行,必须走 Assets API
"""
import os
import base64
import json
import re
import requests
from typing import Dict, List, Optional
from .base_adapter import BaseModelAdapter
@@ -67,8 +68,59 @@ class NvidiaVisionAdapter(BaseModelAdapter):
return False
# ------------------------------------------------------------------
# 整视频分析:base64 整视频 -> video_url 单次调用
# 整视频分析:Assets API 上传 -> video_url(asset_id) 单次调用
# ------------------------------------------------------------------
ASSET_API = "https://api.nvcf.nvidia.com/v2/nvcf/assets"
def _upload_asset(self, video_path: str) -> Optional[str]:
"""用 NVIDIA Assets API 上传大视频文件,返回 asset_id 供 video_url 引用。"""
upload_timeout = max(self.timeout, 900)
try:
r = requests.post(
self.ASSET_API,
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json={"contentType": "video/mp4", "description": "fam-edge video asset"},
timeout=60,
)
except Exception as e:
logger.warning(f"NVIDIA 创建 asset 异常: {e}")
return None
if r.status_code not in (200, 201):
logger.warning(f"NVIDIA 创建 asset 失败 HTTP {r.status_code}: {r.text[:200]}")
return None
try:
j = r.json()
asset_id = j.get("assetId")
upload_url = j.get("uploadUrl")
except ValueError:
logger.warning("NVIDIA asset 响应解析失败")
return None
if not asset_id or not upload_url:
logger.warning("NVIDIA asset 响应缺少 assetId/uploadUrl")
return None
try:
with open(video_path, 'rb') as f:
up = requests.put(
upload_url,
data=f,
headers={"Content-Type": "application/octet-stream"},
timeout=upload_timeout,
)
except requests.Timeout:
logger.warning(f"NVIDIA 上传 asset 超时 ({upload_timeout}s)")
return None
except Exception as e:
logger.warning(f"NVIDIA 上传 asset 异常: {e}")
return None
if up.status_code not in (200, 201):
logger.warning(f"NVIDIA 上传 asset 失败 HTTP {up.status_code}: {up.text[:200]}")
return None
logger.info(f"NVIDIA asset 上传成功: {asset_id}")
return asset_id
def analyze_video(self, video_path: str,
known_members_context: str,
event_start_time: str = '') -> Optional[Dict]:
@@ -82,11 +134,9 @@ class NvidiaVisionAdapter(BaseModelAdapter):
logger.warning(f"NVIDIA 视频文件不存在: {video_path}")
return None
try:
with open(video_path, 'rb') as f:
b64 = base64.b64encode(f.read()).decode('utf-8')
except Exception as e:
logger.warning(f"NVIDIA 读取视频失败: {e}")
asset_id = self._upload_asset(video_path)
if not asset_id:
self._cb.record_failure()
return None
prompt = self._build_video_prompt(known_members_context, event_start_time)
@@ -96,7 +146,7 @@ class NvidiaVisionAdapter(BaseModelAdapter):
messages=[{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "video_url", "video_url": {
"url": f"data:video/mp4;base64,{b64}"}}
"url": f"data:video/mp4;asset_id={asset_id}"}}
]}],
temperature=0.2,
max_tokens=4096,