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

@@ -122,24 +122,52 @@ class GeminiAdapter(BaseModelAdapter):
self._delete_file(file_uri) self._delete_file(file_uri)
def _upload_file(self, video_path: str) -> Optional[str]: def _upload_file(self, video_path: str) -> Optional[str]:
"""用 Files API 上传完整视频,返回可引用 URI。""" """用 Files API resumable 可续传协议上传完整视频,返回可引用 URI。"""
name = os.path.basename(video_path) name = os.path.basename(video_path)
upload_url = f"{self._base_url}/files?key={self.api_key}" size = os.path.getsize(video_path)
upload_timeout = max(self.timeout, 900)
base = f"{self._base_url}/files?key={self.api_key}"
# 1) 创建可续传上传会话
try:
r0 = requests.post(
base,
headers={
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Header-Content-Length": str(size),
"X-Goog-Upload-Header-Content-Type": "video/mp4",
"X-Goog-Upload-File-Name": name,
"Content-Type": "application/json",
},
json={"file": {"display_name": name}},
timeout=60,
)
except Exception as e:
logger.error(f"Gemini 创建上传会话异常: {e}")
return None
if r0.status_code not in (200, 201):
logger.warning(f"Gemini 创建上传会话失败 HTTP {r0.status_code}: {r0.text[:200]}")
return None
session_url = r0.headers.get('X-Goog-Upload-URL')
if not session_url:
logger.warning("Gemini 上传响应缺少 X-Goog-Upload-URL")
return None
# 2) 上传文件体(流式)
try: try:
with open(video_path, 'rb') as f: with open(video_path, 'rb') as f:
data = f.read() resp = requests.post(
except OSError as e: session_url,
logger.error(f"读取视频失败 {video_path}: {e}")
return None
headers={ headers={
"X-Goog-Upload-Protocol": "raw", "Content-Length": str(size),
"X-Goog-Upload-File-Name": name, "X-Goog-Upload-Command": "upload, finalize",
"X-Goog-Upload-Offset": "0",
"Content-Type": "video/mp4", "Content-Type": "video/mp4",
} },
try: data=f,
resp = requests.post(upload_url, headers=headers, data=data, timeout=300) timeout=upload_timeout,
)
except requests.Timeout: except requests.Timeout:
logger.warning("Gemini 文件上传超时 (300s)") logger.warning(f"Gemini 文件上传超时 ({upload_timeout}s)")
return None return None
except Exception as e: except Exception as e:
logger.error(f"Gemini 文件上传异常: {e}") logger.error(f"Gemini 文件上传异常: {e}")

View File

@@ -5,12 +5,13 @@ provider_name = "nvidia"
模型: nvidia/nemotron-nano-12b-v2-vlNIM 官方支持整视频 video_url 输入,内部自行采样帧) 模型: nvidia/nemotron-nano-12b-v2-vlNIM 官方支持整视频 video_url 输入,内部自行采样帧)
角色: vision (整视频直出结构化 JSON) + 智能问答 角色: vision (整视频直出结构化 JSON) + 智能问答
SDK: openai (NIM 兼容 OpenAI API 规范) SDK: openai (NIM 兼容 OpenAI API 规范)
整视频分析: 整视频 base64 经 video_url 单次调用 —— 本地不切片、不抽帧 整视频分析: 先经 NVIDIA Assets API 上传完整视频拿 asset_id再以 video_url 引用单次调用
—— 本地不切片、不抽帧request payload 有 25MB 上限base64 直塞不可行,必须走 Assets API
""" """
import os import os
import base64
import json import json
import re import re
import requests
from typing import Dict, List, Optional from typing import Dict, List, Optional
from .base_adapter import BaseModelAdapter from .base_adapter import BaseModelAdapter
@@ -67,8 +68,59 @@ class NvidiaVisionAdapter(BaseModelAdapter):
return False 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, def analyze_video(self, video_path: str,
known_members_context: str, known_members_context: str,
event_start_time: str = '') -> Optional[Dict]: event_start_time: str = '') -> Optional[Dict]:
@@ -82,11 +134,9 @@ class NvidiaVisionAdapter(BaseModelAdapter):
logger.warning(f"NVIDIA 视频文件不存在: {video_path}") logger.warning(f"NVIDIA 视频文件不存在: {video_path}")
return None return None
try: asset_id = self._upload_asset(video_path)
with open(video_path, 'rb') as f: if not asset_id:
b64 = base64.b64encode(f.read()).decode('utf-8') self._cb.record_failure()
except Exception as e:
logger.warning(f"NVIDIA 读取视频失败: {e}")
return None return None
prompt = self._build_video_prompt(known_members_context, event_start_time) prompt = self._build_video_prompt(known_members_context, event_start_time)
@@ -96,7 +146,7 @@ class NvidiaVisionAdapter(BaseModelAdapter):
messages=[{"role": "user", "content": [ messages=[{"role": "user", "content": [
{"type": "text", "text": prompt}, {"type": "text", "text": prompt},
{"type": "video_url", "video_url": { {"type": "video_url", "video_url": {
"url": f"data:video/mp4;base64,{b64}"}} "url": f"data:video/mp4;asset_id={asset_id}"}}
]}], ]}],
temperature=0.2, temperature=0.2,
max_tokens=4096, max_tokens=4096,