vlm_server.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. import os
  2. import cv2
  3. import base64
  4. import uuid
  5. import shutil
  6. import logging
  7. import requests
  8. from typing import Optional
  9. from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Body
  10. from fastapi.responses import JSONResponse, StreamingResponse
  11. from fastapi.staticfiles import StaticFiles
  12. from fastapi.middleware.cors import CORSMiddleware
  13. from pydantic import BaseModel
  14. from openai import OpenAI
  15. from scenedetect import open_video, SceneManager
  16. from scenedetect.detectors import ContentDetector
  17. import asyncio
  18. from concurrent.futures import ThreadPoolExecutor
  19. from minio import Minio
  20. import traceback
  21. # 配置日志
  22. logging.basicConfig(
  23. level=logging.INFO,
  24. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  25. )
  26. logger = logging.getLogger(__name__)
  27. app = FastAPI()
  28. app.add_middleware(
  29. CORSMiddleware,
  30. allow_origins=["*"],
  31. allow_credentials=True,
  32. allow_methods=["*"],
  33. allow_headers=["*"],
  34. )
  35. class Config:
  36. # API_KEY = "sk-0bfcef4bcb124cba8484bb196a8befc6"
  37. API_KEY = "empety"
  38. # BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
  39. BASE_URL = "http://192.168.3.33:11434/v1"
  40. # MODEL_NAME = "qwen3-vl-flash"
  41. MODEL_NAME = "qwen3-vl:8b"
  42. # 视频处理参数
  43. KEYFRAMES_PER_SCENE = 3 # 每个镜头抽取的关键帧数量
  44. EXTRACT_INTERVAL = 3 # 无镜头检测时的抽帧间隔(秒)
  45. TEMP_DIR = "./temp_file" # 临时文件目录
  46. UPLOAD_DIR = "./uploads" # 上传文件目录
  47. # 提示词模板
  48. PROMPT_TEXT = "请回答用户的问题。"
  49. # PROMPT_IMAGE = "请描述图像中的主要内容、场景与人物。"
  50. PROMPT_VIDEO = "请描述镜头中的主要内容、场景、动作与事件。"
  51. PROMPT_IMAGE = """
  52. 你是一名专业的“应急灾害影像分析员”,需要根据图像内容对以下 6 类应急场景进行检测与风险分析:
  53. 烟火 / 烟雾 / 热异常
  54. 积水 / 涝情
  55. 山体滑坡 / 崩塌 / 灾害迹象
  56. 道路交通状况
  57. 基础设施受损情况
  58. 人员活动 / 救援迹象
  59. 请严格按照以下规则执行任务:
  60. 【任务要求】
  61. 1. 对每一个类别输出一个 JSON 对象,共 6 个
  62. 类别列表:
  63. "fire_smoke_detection"
  64. "water_accumulation"
  65. "landslide_signs"
  66. "road_traffic_status"
  67. "critical_infrastructure"
  68. "human_activity_signs"
  69. 2. 每个类别内部必须包含以下字段(字段名固定):
  70. {
  71. "category": "",
  72. "is_detected": false,
  73. "confidence": null,
  74. "description": "",
  75. "risk_assessment": "",
  76. "location_reference": ""
  77. }
  78. 字段含义如下:
  79. 字段名 说明
  80. category 以上六个类别之一,保持字符串一致
  81. is_detected 是否检测到关键要素(true/false)
  82. confidence [可选] 你对判断的置信度 0~1
  83. description 详细描述:出现的对象、规模、范围、位置、特征
  84. risk_assessment 风险等级:"低" / "中" / "高"
  85. location_reference 相对位置,如:道路北侧、桥梁附近、画面左下角等
  86. 3. 必须严格输出 JSON 数组格式,不得输出解释性文字
  87. [
  88. {... 六个类别的 JSON ...}
  89. ]
  90. 4. 六类检测提示词(模型的视觉分析指南)
  91. 【六类视觉检测指引】
  92. 1. fire_smoke_detection(烟火监测)
  93. 分析要点:
  94. 是否有烟雾、明火、热异常、烧痕
  95. 位置:如“山腰东南方向”“画面左侧”
  96. 烟雾颜色:白/灰/黑
  97. 火势规模:小片区 / 中等 / 大范围
  98. 风险评估:低/中/高
  99. 目标元素: 烟雾、明火、热异常区、过火痕迹
  100. 评估维度: 有无、位置、范围、趋势、风险等级
  101. 2. water_accumulation(积水)
  102. 分析要点:
  103. 路面积水、低洼区积水、河道上涨
  104. 水面范围、估算深度
  105. 是否影响道路、房屋、车辆
  106. 是否有潜在次生灾害(淹没道路、冲刷边坡等)
  107. 目标元素: 路面漫水、低洼积水、河道上涨、排水设施
  108. 评估维度: 有无、位置、范围、深度、风险性
  109. 3. landslide_signs(山体异常)
  110. 分析要点:
  111. 滑坡体、崩塌体、碎屑流、裂缝、树木倾斜
  112. 新鲜裸露土体 / 老滑坡复活迹象
  113. 是否影响道路、河道或建筑
  114. 活动性(新滑坡 / 发展中)
  115. 目标元素: 滑坡体、崩塌、裂缝、碎屑流、植被破坏
  116. 评估维度: 有无、类型、规模、位置、活动性、影响范围
  117. 4. road_traffic_status(道路交通)
  118. 分析要点:
  119. 道路是否通畅、受阻、中断
  120. 是否有事故、拥堵、车辆滞留
  121. 路面损坏:塌陷、裂缝、障碍物
  122. 是否有救援力量
  123. 目标元素: 通行中断、拥堵、车辆滞留、损坏、救援车辆
  124. 评估维度: 通行状态 / 原因 / 影响程度 / 救援通道是否畅通
  125. 5. critical_infrastructure(关键基建)
  126. 分析要点:
  127. 桥梁是否倾斜 / 断裂
  128. 隧道口是否堵塞
  129. 输电线塔是否倒伏
  130. 通信基站是否损坏
  131. 损伤程度及影响功能
  132. 目标元素: 桥梁、隧道、线塔、重要设施
  133. 评估维度: 结构完整性 / 功能状态 / 受损程度
  134. 6. human_activity_signs(人员活动与救援)
  135. 分析要点:
  136. 有无人员:正常/受困
  137. 救援队伍、工程车辆、临时安置点
  138. 位置信息
  139. 该区域是否安全
  140. 目标元素: 受困人员、救援人员、工程车、临时设施
  141. 评估维度: 活动类型 / 人员规模 / 位置 / 安全性
  142. 最终输出格式(必须严格遵守)
  143. 输出示例结构如下(内容仅示意):
  144. [
  145. {
  146. "category": "fire_smoke_detection",
  147. "is_detected": false,
  148. "confidence": 0.12,
  149. "description": "",
  150. "risk_assessment": "低",
  151. "location_reference": ""
  152. },
  153. {
  154. "category": "water_accumulation",
  155. "is_detected": true,
  156. "confidence": 0.78,
  157. "description": "画面右下角出现浅层路面积水,范围较小。",
  158. "risk_assessment": "低",
  159. "location_reference": "画面右下区域"
  160. },
  161. ...
  162. ]
  163. 禁止输出以下内容:
  164. 解释文字
  165. Markdown
  166. 图片描述以外的闲聊
  167. 未定义字段
  168. JSON 外的任何文字
  169. """
  170. # MinIO配置
  171. # MINIO_ENDPOINT = 'xia0miduo.gicp.net:9000'
  172. # MINIO_ACCESS_KEY = 'minioadmin'
  173. # MINIO_SECRET_KEY = 'minioadmin'
  174. # MINIO_BUCKET = 'papbtest'
  175. # MINIO_URL = "http://xia0miduo.gicp.net:9000"
  176. # MINIO_SECURE = False
  177. # MINIO_ENDPOINT = '127.0.0.1:30802'
  178. # MINIO_ACCESS_KEY = 'admin'
  179. # MINIO_SECRET_KEY = 'Ryu304307910'
  180. # MINIO_BUCKET = 'file-storage-privatization'
  181. # MINIO_URL = "http://127.0.0.1:30802"
  182. # MINIO_SECURE = False
  183. # MINIO_ENDPOINT = '192.168.3.33:9000'
  184. # MINIO_ACCESS_KEY = 'minioadmin'
  185. # MINIO_SECRET_KEY = 'minioadmin'
  186. # MINIO_BUCKET = 'dji-fh'
  187. # MINIO_URL = "http://192.168.3.33:9000"
  188. # MINIO_SECURE = False
  189. MINIO_ENDPOINT = 'minio.ryuiso.com:59000'
  190. MINIO_ACCESS_KEY = 'oss_library'
  191. MINIO_SECRET_KEY = 'yDkG9YJiC92G3vk52goST'
  192. MINIO_BUCKET = 'dji-cloudapi'
  193. MINIO_URL = "http://minio.ryuiso.com:59000"
  194. MINIO_SECURE = False
  195. # 创建目录
  196. os.makedirs(Config.TEMP_DIR, exist_ok=True)
  197. os.makedirs(Config.UPLOAD_DIR, exist_ok=True)
  198. # 线程池用于异步处理
  199. executor = ThreadPoolExecutor(max_workers=4)
  200. # MinIO客户端初始化
  201. try:
  202. minio_client = Minio(
  203. Config.MINIO_ENDPOINT,
  204. access_key=Config.MINIO_ACCESS_KEY,
  205. secret_key=Config.MINIO_SECRET_KEY,
  206. secure=Config.MINIO_SECURE,
  207. )
  208. logger.info("MinIO客户端初始化成功")
  209. except Exception as e:
  210. logger.error(f"MinIO客户端初始化失败: {e}")
  211. minio_client = None
  212. def build_image_content(image_path_or_url: str) -> dict:
  213. """
  214. 构建图像请求格式内容
  215. """
  216. if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"):
  217. try:
  218. # 下载网络图片
  219. logger.info(f"正在下载网络图片: {image_path_or_url}")
  220. response = requests.get(image_path_or_url, timeout=30)
  221. response.raise_for_status()
  222. img_bytes = response.content
  223. # 根据Content-Type或URL扩展名确定MIME类型
  224. mime = "image/jpeg" # 默认MIME类型
  225. content_type = response.headers.get('Content-Type', '')
  226. if 'image/png' in content_type or image_path_or_url.lower().endswith(".png"):
  227. mime = "image/png"
  228. elif 'image/gif' in content_type or image_path_or_url.lower().endswith(".gif"):
  229. mime = "image/gif"
  230. elif 'image/webp' in content_type or image_path_or_url.lower().endswith(".webp"):
  231. mime = "image/webp"
  232. # 转换为base64编码
  233. b64 = base64.b64encode(img_bytes).decode("utf-8")
  234. logger.info(f"网络图片下载并转换成功: {image_path_or_url}")
  235. return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
  236. except requests.exceptions.RequestException as e:
  237. logger.error(f"下载网络图片失败: {image_path_or_url}, 错误: {str(e)}")
  238. raise ValueError(f"无法下载网络图片: {image_path_or_url}, 错误: {str(e)}")
  239. except Exception as e:
  240. logger.error(f"处理网络图片时出错: {image_path_or_url}, 错误: {str(e)}")
  241. raise ValueError(f"处理网络图片时出错: {image_path_or_url}, 错误: {str(e)}")
  242. elif os.path.exists(image_path_or_url):
  243. with open(image_path_or_url, "rb") as f:
  244. img_bytes = f.read()
  245. # 转码
  246. b64 = base64.b64encode(img_bytes).decode("utf-8")
  247. # 根据文件扩展名确定MIME类型
  248. mime = "image/jpeg"
  249. if image_path_or_url.lower().endswith(".png"):
  250. mime = "image/png"
  251. elif image_path_or_url.lower().endswith(".gif"):
  252. mime = "image/gif"
  253. elif image_path_or_url.lower().endswith(".webp"):
  254. mime = "image/webp"
  255. return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
  256. else:
  257. raise ValueError(f"无效的图片路径或URL: {image_path_or_url}")
  258. def detect_scenes(video_path: str) -> list:
  259. """
  260. 使用PySceneDetect检测视频中的镜头切换
  261. """
  262. # 有关PySceneDetect的镜头检测,详见https://www.scenedetect.com/docs/latest/api/scene_manager.html#usage
  263. video = open_video(video_path)
  264. scene_manager = SceneManager()
  265. scene_manager.add_detector(ContentDetector(threshold=20.0))
  266. scene_manager.detect_scenes(video)
  267. scenes = scene_manager.get_scene_list()
  268. print(f"检测到 {len(scenes)} 个镜头片段")
  269. return scenes
  270. def extract_frames_every(video_path: str, seconds: int = 3) -> list:
  271. """
  272. 按固定时间间隔从视频中提取帧(没有检测到镜头时)
  273. """
  274. cap = cv2.VideoCapture(video_path)
  275. fps = cap.get(cv2.CAP_PROP_FPS)
  276. interval = int(fps * seconds)
  277. frame_paths = []
  278. count = 0
  279. while cap.isOpened():
  280. ret, frame = cap.read()
  281. if not ret:
  282. break
  283. if count % interval == 0:
  284. path = f"{Config.TEMP_DIR}/frame_{count//interval+1}.jpg"
  285. cv2.imwrite(path, frame)
  286. frame_paths.append(path)
  287. count += 1
  288. cap.release()
  289. print(f"基于间隔抽取 {len(frame_paths)} 张帧")
  290. return frame_paths
  291. def extract_keyframes(video_path: str, scenes: list, num_frames: int = 3) -> list:
  292. """
  293. 从每个检测到的镜头中提取关键帧
  294. """
  295. cap = cv2.VideoCapture(video_path)
  296. keyframes = []
  297. for i, (start, end) in enumerate(scenes):
  298. start_s, end_s = start.get_seconds(), end.get_seconds()
  299. duration = end_s - start_s
  300. frames = []
  301. for j in range(num_frames):
  302. # 在镜头时间范围内均匀抽帧
  303. t = start_s + (j + 1) * duration / (num_frames + 1)
  304. cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
  305. ret, frame = cap.read()
  306. if ret:
  307. gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
  308. # 过滤过暗的帧
  309. if gray.mean() > 30:
  310. path = f"{Config.TEMP_DIR}/scene{i+1}_{j+1}.jpg"
  311. cv2.imwrite(path, frame)
  312. frames.append(path)
  313. if frames:
  314. keyframes.append(frames)
  315. cap.release()
  316. return keyframes
  317. def describe_scene(client: OpenAI, model_name: str, image_paths: list, scene_idx: int, prompt: str = None) -> str:
  318. """
  319. 调用多模态模型描述单个场景
  320. """
  321. if not prompt:
  322. prompt = Config.PROMPT_VIDEO
  323. contents = [build_image_content(p) for p in image_paths]
  324. contents.append({"type": "text", "text": f"{prompt}(镜头 {scene_idx})"})
  325. response = client.chat.completions.create(
  326. model=model_name,
  327. messages=[{"role": "user", "content": contents}],
  328. max_tokens=5120,
  329. extra_body={"mm_processor_kwargs": {"fps": 2, "do_sample_frames": True}},
  330. )
  331. return response.choices[0].message.content
  332. def summarize_video(client: OpenAI, model_name: str, scene_descriptions: list) -> str:
  333. """
  334. 将所有场景描述汇总为整体视频摘要
  335. """
  336. prompt = "以下是视频中每个镜头的描述,请总结视频的主要内容与主题:\n\n"
  337. for i, d in enumerate(scene_descriptions):
  338. prompt += f"镜头{i+1}: {d}\n"
  339. prompt += "\n请生成简洁的整体视频摘要。"
  340. response = client.chat.completions.create(
  341. model=model_name,
  342. messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}],
  343. max_tokens=5120
  344. )
  345. return response.choices[0].message.content
  346. def detect_content_type(input_path: str) -> str:
  347. """
  348. 自动检测输入内容的类型
  349. """
  350. # 仅通过文件扩展名判断
  351. if os.path.isfile(input_path):
  352. ext = os.path.splitext(input_path)[1].lower()
  353. if ext in [".mp4", ".mov", ".avi", ".mkv"]:
  354. return "video"
  355. elif ext in [".jpg", ".jpeg", ".png", ".gif"]:
  356. return "image"
  357. else:
  358. return "text"
  359. # 通过URL判断
  360. elif input_path.startswith("http://") or input_path.startswith("https://"):
  361. if any(x in input_path.lower() for x in [".mp4", ".mov", ".avi", ".mkv"]):
  362. return "video"
  363. elif any(x in input_path.lower() for x in [".jpg", ".jpeg", ".png", ".gif"]):
  364. return "image"
  365. else:
  366. return "text"
  367. else:
  368. return "text"
  369. def process_content(
  370. content: str,
  371. content_type: Optional[str] = None,
  372. keyframes_per_scene: int = 3,
  373. extract_interval: int = 3,
  374. prompt_text: str = None,
  375. prompt_image: str = None,
  376. prompt_video: str = None
  377. ) -> dict:
  378. """
  379. 处理内容分析
  380. """
  381. try:
  382. logger.info(f"开始处理内容: {content[:100]}...")
  383. # 使用默认提示词(如果未提供)
  384. if not prompt_text:
  385. prompt_text = Config.PROMPT_TEXT
  386. if not prompt_image:
  387. prompt_image = Config.PROMPT_IMAGE
  388. if not prompt_video:
  389. prompt_video = Config.PROMPT_VIDEO
  390. logger.info("初始化OpenAI客户端...")
  391. client = OpenAI(api_key=Config.API_KEY, base_url=Config.BASE_URL, timeout=3600)
  392. # 自动检测内容类型
  393. if not content_type:
  394. content_type = detect_content_type(content)
  395. logger.info(f"检测到内容类型: {content_type}")
  396. print(f"内容类型: {content_type}")
  397. except Exception as e:
  398. logger.error(f"process_content初始化失败: {str(e)}")
  399. logger.error(traceback.format_exc())
  400. raise
  401. # 根据类型处理
  402. if content_type == "text":
  403. try:
  404. logger.info("处理文本内容...")
  405. messages = [{"role": "user", "content": [{"type": "text", "text": f"{prompt_text}\n\n{content}"}]}]
  406. resp = client.chat.completions.create(model=Config.MODEL_NAME, messages=messages, max_tokens=5120)
  407. logger.info("文本内容处理成功")
  408. return {
  409. "status": "success",
  410. "content_type": "text",
  411. "result": resp.choices[0].message.content,
  412. "scenes": None
  413. }
  414. except Exception as e:
  415. logger.error(f"文本处理失败: {str(e)}")
  416. raise
  417. elif content_type == "image":
  418. try:
  419. logger.info("处理图像内容...")
  420. content_obj = build_image_content(content)
  421. messages = [{"role": "user", "content": [content_obj, {"type": "text", "text": prompt_image}]}]
  422. resp = client.chat.completions.create(model=Config.MODEL_NAME, messages=messages, max_tokens=5120)
  423. logger.info("图像内容处理成功")
  424. return {
  425. "status": "success",
  426. "content_type": "image",
  427. "result": resp.choices[0].message.content,
  428. "scenes": None
  429. }
  430. except Exception as e:
  431. logger.error(f"图像处理失败: {str(e)}")
  432. raise
  433. elif content_type == "video":
  434. try:
  435. # 视频处理流程
  436. logger.info("开始视频处理...")
  437. print("开始镜头检测...")
  438. scenes = detect_scenes(content)
  439. # 没检测到镜头
  440. if not scenes:
  441. logger.info("未检测到镜头,使用时间间隔抽帧")
  442. print("未检测到镜头,使用时间间隔抽帧...")
  443. frames = extract_frames_every(content, seconds=extract_interval)
  444. keyframes = [[f] for f in frames]
  445. else:
  446. logger.info(f"检测到{len(scenes)}个镜头")
  447. print("提取关键帧...")
  448. keyframes = extract_keyframes(content, scenes, num_frames=keyframes_per_scene)
  449. logger.info("开始场景分析...")
  450. print("调用模型分析场景...")
  451. scene_descriptions = []
  452. for i, frames in enumerate(keyframes):
  453. print(f"分析镜头 {i+1}/{len(keyframes)}...")
  454. logger.info(f"分析镜头 {i+1}/{len(keyframes)}")
  455. desc = describe_scene(client, Config.MODEL_NAME, frames, i + 1, prompt_video)
  456. scene_descriptions.append(desc)
  457. print(f"镜头{i+1}: {desc}")
  458. logger.info("生成视频摘要...")
  459. print("生成视频摘要...")
  460. summary = summarize_video(client, Config.MODEL_NAME, scene_descriptions)
  461. logger.info("视频处理完成")
  462. return {
  463. "status": "success",
  464. "content_type": "video",
  465. "result": summary,
  466. "scenes": scene_descriptions
  467. }
  468. except Exception as e:
  469. logger.error(f"视频处理失败: {str(e)}")
  470. raise
  471. else:
  472. logger.error(f"不支持的内容类型: {content_type}")
  473. raise ValueError(f"不支持的内容类型: {content_type}")
  474. # Pydantic模型定义请求体
  475. class AnalyzeRequest(BaseModel):
  476. content: str
  477. content_type: Optional[str] = None
  478. keyframes_per_scene: Optional[int] = 3
  479. extract_interval: Optional[int] = 3
  480. prompt_text: Optional[str] = None
  481. prompt_image: Optional[str] = None
  482. prompt_video: Optional[str] = None
  483. @app.post("/analyze")
  484. async def analyze_content(request: AnalyzeRequest):
  485. """
  486. 分析文本、URL或已有路径的内容
  487. """
  488. try:
  489. logger.info(f"收到/analyze请求: content={request.content[:100] if len(request.content) > 100 else request.content}...")
  490. logger.info(f"请求参数: content_type={request.content_type}, keyframes_per_scene={request.keyframes_per_scene}, extract_interval={request.extract_interval}")
  491. # 验证content参数
  492. if not request.content:
  493. logger.error("请求缺少content参数")
  494. raise HTTPException(status_code=400, detail="缺少content参数")
  495. # 在线程池中执行处理
  496. loop = asyncio.get_running_loop()
  497. logger.info("开始异步处理内容...")
  498. result = await loop.run_in_executor(
  499. executor,
  500. process_content,
  501. request.content,
  502. request.content_type,
  503. request.keyframes_per_scene,
  504. request.extract_interval,
  505. request.prompt_text,
  506. request.prompt_image,
  507. request.prompt_video
  508. )
  509. logger.info(f"处理完成: {result['status']}")
  510. return result
  511. except HTTPException as he:
  512. logger.error(f"HTTP异常: {he.status_code} - {he.detail}")
  513. raise
  514. except Exception as e:
  515. logger.error(f"处理请求时发生异常: {str(e)}")
  516. logger.error(f"异常详情: {traceback.format_exc()}")
  517. raise HTTPException(status_code=500, detail=str(e))
  518. @app.get("/minio/files")
  519. async def list_minio_files():
  520. """
  521. 获取MinIO中的文件列表
  522. """
  523. try:
  524. objects = minio_client.list_objects(Config.MINIO_BUCKET, recursive=True)
  525. files = []
  526. for obj in objects:
  527. # 只获取图片和视频文件
  528. ext = os.path.splitext(obj.object_name)[1].lower()
  529. if ext in ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov', '.avi', '.mkv']:
  530. file_url = f"{Config.MINIO_URL}/{Config.MINIO_BUCKET}/{obj.object_name}"
  531. files.append({
  532. "name": obj.object_name,
  533. "size": obj.size,
  534. "url": file_url,
  535. "type": "image" if ext in ['.jpg', '.jpeg', '.png', '.gif'] else "video"
  536. })
  537. print(f"文件: {obj.object_name}, 大小: {obj.size}, URL: {file_url}")
  538. return {"files": files}
  539. except Exception as e:
  540. logger.error(f"获取MinIO文件列表时发生异常: {str(e)}")
  541. raise HTTPException(status_code=500, detail=str(e))
  542. except Exception as e:
  543. raise HTTPException(status_code=500, detail=str(e))
  544. # @app.post("/upload")
  545. # async def upload_and_analyze(
  546. # file: UploadFile = File(...),
  547. # content_type: Optional[str] = Form(None),
  548. # keyframes_per_scene: Optional[int] = Form(3),
  549. # extract_interval: Optional[int] = Form(3),
  550. # prompt_text: Optional[str] = Form(None),
  551. # prompt_image: Optional[str] = Form(None),
  552. # prompt_video: Optional[str] = Form(None)
  553. # ):
  554. # """
  555. # 上传文件并进行分析
  556. # """
  557. # try:
  558. # # 生成唯一文件名
  559. # file_id = str(uuid.uuid4())
  560. # file_ext = os.path.splitext(file.filename)[1]
  561. # file_path = os.path.join(Config.UPLOAD_DIR, f"{file_id}{file_ext}")
  562. #
  563. # # 保存上传的文件
  564. # with open(file_path, "wb") as buffer:
  565. # shutil.copyfileobj(file.file, buffer)
  566. #
  567. # print(f"文件已保存: {file_path}")
  568. #
  569. # # 在线程池中执行处理
  570. # loop = asyncio.get_event_loop()
  571. # result = await loop.run_in_executor(
  572. # executor,
  573. # process_content,
  574. # file_path,
  575. # content_type,
  576. # keyframes_per_scene,
  577. # extract_interval,
  578. # prompt_text,
  579. # prompt_image,
  580. # prompt_video
  581. # )
  582. #
  583. # return JSONResponse(content=result)
  584. # except Exception as e:
  585. # raise HTTPException(status_code=500, detail=str(e))
  586. if __name__ == "__main__":
  587. import uvicorn
  588. uvicorn.run(app, host="0.0.0.0", port=8000)