From d54070ece464728129496e3d3c4f23cce19f8924 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Fri, 21 Aug 2026 11:37:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(fam-edge):=20=E6=95=B4=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=B9=E5=BC=8F=E4=BF=AE=E6=AD=A3=20-=20Ge?= =?UTF-8?q?mini=20=E6=94=B9=20resumable=20=E5=8F=AF=E7=BB=AD=E4=BC=A0?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE(370MB=20=E4=B8=8D=E5=86=8D=20raw=20=E5=8D=95?= =?UTF-8?q?=E5=8F=91=E8=B6=85=E6=97=B6)=EF=BC=9BNVIDIA=20=E6=94=B9=20Asset?= =?UTF-8?q?s=20API=20=E4=B8=8A=E4=BC=A0=E6=8B=BF=20asset=5Fid(video=5Furl?= =?UTF-8?q?=20=E5=BC=95=E7=94=A8)=EF=BC=8C=E8=A7=84=E9=81=BF=2025MB=20payl?= =?UTF-8?q?oad=20=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fam_edge/model_adapters/gemini_adapter.py | 56 +++++++++++---- .../fam_edge/model_adapters/nvidia_adapter.py | 68 ++++++++++++++++--- 2 files changed, 101 insertions(+), 23 deletions(-) diff --git a/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py b/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py index 49ff714..9efe515 100644 --- a/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py +++ b/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py @@ -122,24 +122,52 @@ class GeminiAdapter(BaseModelAdapter): self._delete_file(file_uri) def _upload_file(self, video_path: str) -> Optional[str]: - """用 Files API 上传完整视频,返回可引用 URI。""" + """用 Files API resumable 可续传协议上传完整视频,返回可引用 URI。""" 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: with open(video_path, 'rb') as f: - data = f.read() - except OSError as e: - logger.error(f"读取视频失败 {video_path}: {e}") - return None - headers = { - "X-Goog-Upload-Protocol": "raw", - "X-Goog-Upload-File-Name": name, - "Content-Type": "video/mp4", - } - try: - resp = requests.post(upload_url, headers=headers, data=data, timeout=300) + resp = requests.post( + session_url, + headers={ + "Content-Length": str(size), + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": "0", + "Content-Type": "video/mp4", + }, + data=f, + timeout=upload_timeout, + ) except requests.Timeout: - logger.warning("Gemini 文件上传超时 (300s)") + logger.warning(f"Gemini 文件上传超时 ({upload_timeout}s)") return None except Exception as e: logger.error(f"Gemini 文件上传异常: {e}") diff --git a/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py b/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py index 6542d30..113d79c 100644 --- a/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py +++ b/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py @@ -5,12 +5,13 @@ provider_name = "nvidia" 模型: nvidia/nemotron-nano-12b-v2-vl(NIM 官方支持整视频 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,