From f557be8b7ecf465531797987ca34ea9d934dffed Mon Sep 17 00:00:00 2001 From: zydi Date: Thu, 3 Apr 2025 06:21:55 +0000 Subject: [PATCH] update 20250403 --- .gitignore | 6 + README.md | 48 +- api/__pycache__/config.cpython-312.pyc | Bin 0 -> 2269 bytes api/compare.py | 121 ++-- api/config.py | 101 ++++ api/cpm_analyze.py | 15 +- api/cpm_scene.py | 20 +- api/face.py | 132 +++-- api/fall.py | 14 +- api/media.py | 14 +- api/pose.py | 60 +- api/qwenvl_analyze.py | 25 +- api/qwenvl_scene.py | 25 +- api/requirements.txt | 16 + api/start_services.sh | 36 ++ api/stop.sh | 19 + api/yolo.py | 70 +-- api_chat/.env | 59 +- api_chat/GPT_SoVITS/.gitkeep | 0 api_chat/OpenBMB/.gitkeep | 0 api_chat/asr.py | 5 +- api_chat/before/asr.py | 115 ---- api_chat/before/chat.py | 117 ---- api_chat/before/mini3_api.py | 182 ------ api_chat/before/mp4_to_wav.py | 63 --- api_chat/before/ollama_api.py | 170 ------ api_chat/before/ollamas.py | 136 ----- api_chat/before/producer_chat_1.py | 319 ----------- api_chat/before/sovits_api.py | 180 ------ api_chat/before/sovits_api_1.py | 180 ------ api_chat/before/tts1.py | 176 ------ api_chat/before/wav_to_text.py | 68 --- api_chat/before/weight.json | 1 - api_chat/before/whisper_api.py | 186 ------- api_chat/chat.py | 32 +- api_chat/docs/.gitkeep | 0 api_chat/runtime/.gitkeep | 0 api_chat/tools/.gitkeep | 0 api_chat/weight.json | 1 + api_history/OpenBMB/.gitkeep | 0 api_history/function/config.yaml | 35 -- api_history/function/cpm.py | 362 ------------ api_history/function/pose.py | 183 ------ api_history/function/producer-minio.py | 204 ------- api_history/function/sync.py | 91 --- api_history/local/local_cpm.py | 299 ---------- api_history/local/local_pose.py | 194 ------- api_history/main.py | 426 -------------- api_history/mini_douyin_time.py | 255 --------- api_history/mini_up.py | 248 --------- api_history/minicpmv2.6.py | 264 --------- api_history/sound.py | 121 ---- api_history/sound_result copy.py | 114 ---- api_history/sound_result.py | 114 ---- api_history/video_s3.py | 256 --------- api_old/__pycache__/config.cpython-310.pyc | Bin 1256 -> 0 bytes api_old/__pycache__/config.cpython-311.pyc | Bin 1677 -> 0 bytes api_old/__pycache__/cpm_api.cpython-311.pyc | Bin 20509 -> 0 bytes api_old/__pycache__/face_api.cpython-311.pyc | Bin 17857 -> 0 bytes api_old/__pycache__/fall_api.cpython-311.pyc | Bin 16923 -> 0 bytes api_old/__pycache__/mediapipe.cpython-310.pyc | Bin 8375 -> 0 bytes api_old/__pycache__/mediapipe.cpython-311.pyc | Bin 17377 -> 0 bytes .../__pycache__/mediapipe_api.cpython-311.pyc | Bin 18871 -> 0 bytes api_old/__pycache__/pose.cpython-311.pyc | Bin 17853 -> 0 bytes .../__pycache__/qwenvl_api.cpython-311.pyc | Bin 21144 -> 0 bytes api_old/__pycache__/yolo_api.cpython-311.pyc | Bin 18940 -> 0 bytes .../__pycache__/mediapipe.cpython-311.pyc | Bin 17394 -> 0 bytes api_old/before/cpm_api.py | 369 ------------ api_old/before/cpm_key.py | 518 ----------------- api_old/before/cpm_key2.py | 526 ------------------ api_old/before/face_api.py | 312 ----------- api_old/before/face_key.py | 462 --------------- api_old/before/face_key2.py | 471 ---------------- api_old/before/fall_api.py | 293 ---------- api_old/before/fall_key.py | 442 --------------- api_old/before/fall_key2.py | 449 --------------- api_old/before/mediapipe.py | 297 ---------- api_old/before/mediapipe_api.py | 304 ---------- api_old/before/mediapipe_key.py | 453 --------------- api_old/before/mediapipe_key2.py | 462 --------------- api_old/before/pose_api.py | 312 ----------- api_old/before/pose_key.py | 461 --------------- api_old/before/pose_key2.py | 470 ---------------- api_old/before/qwenvl_api.py | 356 ------------ api_old/before/qwenvl_key.py | 508 ----------------- api_old/before/qwenvl_key2.py | 518 ----------------- api_old/before/yolo_api.py | 315 ----------- api_old/before/yolo_key.py | 462 --------------- api_old/before/yolo_key2.py | 472 ---------------- api_old/config.py | 81 --- api_old/producer.py | 419 -------------- chat_history/ChatTTS/.gitkeep | 0 chat_history/chat_ser2.py | 88 --- chat_history/chattts_service.py | 92 --- chat_history/kafka_chat/demo.py | 49 -- chat_history/kafka_chat/text_input_db.py | 210 ------- chat_history/kafka_chat/voice_input.py | 169 ------ chat_history/kafka_chat/voice_output.py | 202 ------- chat_history/kafka_chat/weight.json | 1 - chat_history/producer_chat.py | 406 -------------- chat_history/qwen_service.py | 96 ---- chat_history/sovit.py | 49 -- chat_history/whisper_service.py | 25 - producer/__pycache__/config.cpython-311.pyc | Bin 0 -> 1721 bytes {api/producer => producer}/config.py | 42 +- {api/producer => producer}/producer.py | 0 {api/producer => producer}/requirements.txt | 0 .../producer_chat => producer_chat}/.env | 56 +- .../producer_chat.py | 0 .../requirements.txt | 0 110 files changed, 569 insertions(+), 16526 deletions(-) create mode 100644 api/__pycache__/config.cpython-312.pyc create mode 100644 api/config.py create mode 100644 api/requirements.txt create mode 100755 api/start_services.sh create mode 100755 api/stop.sh mode change 100644 => 100755 api_chat/GPT_SoVITS/.gitkeep delete mode 100644 api_chat/OpenBMB/.gitkeep delete mode 100644 api_chat/before/asr.py delete mode 100644 api_chat/before/chat.py delete mode 100644 api_chat/before/mini3_api.py delete mode 100644 api_chat/before/mp4_to_wav.py delete mode 100644 api_chat/before/ollama_api.py delete mode 100644 api_chat/before/ollamas.py delete mode 100644 api_chat/before/producer_chat_1.py delete mode 100644 api_chat/before/sovits_api.py delete mode 100644 api_chat/before/sovits_api_1.py delete mode 100644 api_chat/before/tts1.py delete mode 100644 api_chat/before/wav_to_text.py delete mode 100644 api_chat/before/weight.json delete mode 100644 api_chat/before/whisper_api.py mode change 100644 => 100755 api_chat/docs/.gitkeep mode change 100644 => 100755 api_chat/runtime/.gitkeep mode change 100644 => 100755 api_chat/tools/.gitkeep create mode 100644 api_chat/weight.json delete mode 100644 api_history/OpenBMB/.gitkeep delete mode 100644 api_history/function/config.yaml delete mode 100644 api_history/function/cpm.py delete mode 100644 api_history/function/pose.py delete mode 100644 api_history/function/producer-minio.py delete mode 100644 api_history/function/sync.py delete mode 100644 api_history/local/local_cpm.py delete mode 100644 api_history/local/local_pose.py delete mode 100644 api_history/main.py delete mode 100644 api_history/mini_douyin_time.py delete mode 100644 api_history/mini_up.py delete mode 100644 api_history/minicpmv2.6.py delete mode 100644 api_history/sound.py delete mode 100644 api_history/sound_result copy.py delete mode 100644 api_history/sound_result.py delete mode 100644 api_history/video_s3.py delete mode 100644 api_old/__pycache__/config.cpython-310.pyc delete mode 100644 api_old/__pycache__/config.cpython-311.pyc delete mode 100644 api_old/__pycache__/cpm_api.cpython-311.pyc delete mode 100644 api_old/__pycache__/face_api.cpython-311.pyc delete mode 100644 api_old/__pycache__/fall_api.cpython-311.pyc delete mode 100644 api_old/__pycache__/mediapipe.cpython-310.pyc delete mode 100644 api_old/__pycache__/mediapipe.cpython-311.pyc delete mode 100644 api_old/__pycache__/mediapipe_api.cpython-311.pyc delete mode 100644 api_old/__pycache__/pose.cpython-311.pyc delete mode 100644 api_old/__pycache__/qwenvl_api.cpython-311.pyc delete mode 100644 api_old/__pycache__/yolo_api.cpython-311.pyc delete mode 100644 api_old/before/__pycache__/mediapipe.cpython-311.pyc delete mode 100644 api_old/before/cpm_api.py delete mode 100644 api_old/before/cpm_key.py delete mode 100644 api_old/before/cpm_key2.py delete mode 100644 api_old/before/face_api.py delete mode 100644 api_old/before/face_key.py delete mode 100644 api_old/before/face_key2.py delete mode 100644 api_old/before/fall_api.py delete mode 100644 api_old/before/fall_key.py delete mode 100644 api_old/before/fall_key2.py delete mode 100644 api_old/before/mediapipe.py delete mode 100644 api_old/before/mediapipe_api.py delete mode 100644 api_old/before/mediapipe_key.py delete mode 100644 api_old/before/mediapipe_key2.py delete mode 100644 api_old/before/pose_api.py delete mode 100644 api_old/before/pose_key.py delete mode 100644 api_old/before/pose_key2.py delete mode 100644 api_old/before/qwenvl_api.py delete mode 100644 api_old/before/qwenvl_key.py delete mode 100644 api_old/before/qwenvl_key2.py delete mode 100644 api_old/before/yolo_api.py delete mode 100644 api_old/before/yolo_key.py delete mode 100644 api_old/before/yolo_key2.py delete mode 100644 api_old/config.py delete mode 100644 api_old/producer.py delete mode 100644 chat_history/ChatTTS/.gitkeep delete mode 100644 chat_history/chat_ser2.py delete mode 100644 chat_history/chattts_service.py delete mode 100644 chat_history/kafka_chat/demo.py delete mode 100644 chat_history/kafka_chat/text_input_db.py delete mode 100644 chat_history/kafka_chat/voice_input.py delete mode 100644 chat_history/kafka_chat/voice_output.py delete mode 100644 chat_history/kafka_chat/weight.json delete mode 100644 chat_history/producer_chat.py delete mode 100644 chat_history/qwen_service.py delete mode 100644 chat_history/sovit.py delete mode 100644 chat_history/whisper_service.py create mode 100644 producer/__pycache__/config.cpython-311.pyc rename {api/producer => producer}/config.py (65%) mode change 100644 => 100755 rename {api/producer => producer}/producer.py (100%) mode change 100644 => 100755 rename {api/producer => producer}/requirements.txt (100%) mode change 100644 => 100755 rename {api_chat/producer_chat => producer_chat}/.env (67%) mode change 100644 => 100755 rename {api_chat/producer_chat => producer_chat}/producer_chat.py (100%) mode change 100644 => 100755 rename {api_chat/producer_chat => producer_chat}/requirements.txt (100%) mode change 100644 => 100755 diff --git a/.gitignore b/.gitignore index 48c1709..5b5b75b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ api_chat/runtime/* api_chat/docs/* !api_chat/docs/.gitkeep +api_chat/GPT_weights +api_chat/GPT_weights_v2 +api_chat/SoVITS_weights +api_chat/SoVITS_weights_v2 + +api/logs \ No newline at end of file diff --git a/README.md b/README.md index 1f9b218..10b0bcb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ ``` API/ ├── api/ # 视觉分析和处理模块 - │ ├── producer/ # 生产者,分配任务 │ ├── cpm_analyze.py # CPM_OCR分析 │ ├── qwenvl_analyze.py # QwenVL_OCR分析 │ ├── cpm_scene.py # CPM_场景分析 @@ -17,22 +16,23 @@ │ ├── face.py # 人脸检测 │ ├── fall.py # 跌倒检测 │ ├── pose.py # 姿态估计 - │ └── media.py # mediapipe 面部特征提取 + │ ├── media.py # mediapipe 面部特征提取 + | ├── start_services.sh #一键开始所有程序 + | └── stop.sh #一键停止所有程序 ├── api_chat/ # 聊天和语音处理模块 - │ ├── producer_chat/ # 聊天生产者 - │ ├── chat.py # 聊天功能 - │ ├── tts.py # 文字转语音 - │ ├── asr.py # 语音识别 + │ ├── chat.py # 聊天功能 + │ ├── tts.py # 文字转语音 + │ ├── asr.py # 语音识别 │ ├── GPT_SoVITS/ # GPT_SoVITS模型集成, │ ├── sample/ # OpenBMB模型——学习音色,音色+文本内容 │ ├── tools/ # GPT_SoVITS模型——工具函数 │ ├── runtime/ # GPT_SoVITS模型——运行时函数 │ ├── docs/ # GPT_SoVITS模型——文档 - │ ├── TEMP/ # OpenBMB模型临时文件夹 - │ └── before/ # 历史代码,可以忽略 - ├── api_history/ # api历史代码,可以忽略 - ├── chat_history/ # api_chat历史代码,可以忽略 - └── api_old/ # api历史代码,可以忽略 + | └── weight.json # GPT_SoVITS模型——权重 + | + ├── producer_chat/ # 聊天生产者 + ├── producer/ # 算法生产者,分配任务 + └── README # 说明文档 ``` ## 主要功能 @@ -52,10 +52,10 @@ - 多模型支持(通过Ollama) ## 使用说明 -### API 部分 http://dev.obscura.work/v1 +### API 部分 http://dev2.obscura.work/v1 1. producer 目录 # 生产者,分配任务 - 2. 服务器:222.186.10.253:8005 - 3. kafka 配置:222.186.10.253:9092 + 2. 服务器:222.186.20.67:8005 + 3. kafka 配置:222.186.20.67:9092 topic分配: - yolo: "yolo" - pose: "pose" @@ -68,7 +68,7 @@ - mediapipe: "mediapipe" - compare: "compare" - 4. redis 配置:150.158.144.159:13003 + 4. redis 配置:222.186.20.67:6379 db分配: - 4: "yolo" - 5: "pose" @@ -82,26 +82,26 @@ - 30: "compare" 5. 模型配置: - - YOLO = "/obscura/models/yolov8x.pt" - - POSE = "/obscura/models/yolov8x-pose.pt" - - QWEN = "/obscura/models/qwen/Qwen2-VL-2B-Instruct" + - YOLO = "/obscura/models/yolo11n.pt" + - POSE = "/obscura/models/yolo11n-pose.pt" + - QWEN = "/obscura/models/qwen/Qwen2.5-VL-7B-Instruct" - FALL = "/obscura/models/yolov8n-fall.pt" - - FACE = "/obscura/models/yolov8n-face.pt" + - FACE = "/obscura/models/yolo11n-face.pt" - MEDIAPIPE = "/obscura/models/face_landmarker.task" - - CPM(ollama) = "https://ffgregevrdcfyhtnhyudvr.myfastools.com/api/generate" + - CPM(ollama) = "https://222.186.20.67:11435/api/generate" 6. 上传文件及结果保存目录: - UPLOAD_DIR = "/obscura/task/upload" - RESULT_DIR = "/obscura/task/result" -### API_Chat 部分 http://dev.obscura.work/v1_chat +### API_Chat 部分 http://dev2.obscura.work/v1_chat 1. producer_chat 目录 # 聊天生产者 - 2. 服务器:222.186.10.253:8008 - 3. kafka 配置:222.186.10.253:9092 + 2. 服务器:222.186.20.67:8008 + 3. kafka 配置:222.186.20.67:9092 topic分配: - asr: "asr" - chat: "chat" - tts: "tts" - 4. redis 配置:150.158.144.159:13003 + 4. redis 配置:222.186.20.67:6379 db分配: - 2: "api key" - 3: "api使用情况" diff --git a/api/__pycache__/config.cpython-312.pyc b/api/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa6c8fe72dc16ea390312727453ec0a05302cf46 GIT binary patch literal 2269 zcma)7&rcgi6rMHUn6=k7#(;rKwz8@B_i@3zVChSefwr-JTv}-<5)zV z%@=PDe(p!;Un;os}z=fUQ#x5AZZt&m%7{ng%;=uz1 zL%%ZEOL{R+Z(odc7Rlb%WG?icR7Rk{<4#NzEq(xUCBFh!u@ z4J$@9T$8Ft;d-U0NN_o;%GG+Ywi{qHoBUm&S`(F{#@P9bB?aVSHGHfTm1wjas?-|( zD{8@tQkAJ8c-e6OVR9#Y|692n6Gd?`_#hozUJs_q)tXw*U(xT`N;$YM6^kDoa9_&n z1I}I^kQT(ERECnI9?5Em&c5Ngwt?ODe8efQqrAi@uAeUuTs>vTQ zE_Q2N>ejg2t#PGW<7$`2c%)ln)MV6p>Go((lPwppX!av%|40&RN~Mt3Y^n@}st8ZC zUb;QnH;6~Gena-cvtoOO3a24NsVo(b8*+P=CVTRgQu`*2>?As`&CyIR-52foPMDY5 zw`ka3A_I}%tRlAqRA0+0rHZ7=ZF=SdtZE~q()&wwQgBo7Q1DV1ra*5bb&SGw0u5`v z4$@kr!_f}MI=s-~c!w7|ywu_44zC2JHH%W!tSZ@#N=b8U@f%ybxSkcZl38uYP@7p{ zFC(TBVkVp1NIlZnY%-C`iT8xu?pd4>vb&no$Y=Om?xB!PXxuiR+7XRpVqJ3>5uZtk zM0qaMUXI^P(j2>&Neg^JOr)~3ac(cYtH-vIbVSxqt!l2Xg|r}U3yEY}%<#MSG*?E* zB~1x>oK1-hKApalxNCaFyG`apJ}YC&4knVx%m#0^+J$tQ-{!@=Y&xe|^M^_yFKg^Y zQgf1-lcf{y3OgIA&78*F-AnLdBKaUi8b!=9YL+pxTrkVHSuUF8l36aB<;rj99oaMJ zy+3@Yl;rc9hRj(%DS4&5UpNR=j^EopCpT)nD1WK06F>dysY(P-dl-g!XGIMAH{wpM z$UWP1w0!;(G(?1RqRF 0: + kpts_scaled = kpts.xy[0].cpu().numpy() * np.array([scale_x, scale_y]) + result["keypoints"] = kpts_scaled.tolist() + + formatted_results.append(result) + except Exception as e: + print(f"处理单个检测结果时出错: {str(e)}") + continue - # 调整边界框坐标以适应原始图像大小 - orig_h, orig_w = original_shape[:2] - model_h, model_w = r.orig_shape - scale_x, scale_y = orig_w / model_w, orig_h / model_h - - bbox = box.xyxy[0].cpu().numpy() - bbox_scaled = [ - bbox[0] * scale_x, bbox[1] * scale_y, - bbox[2] * scale_x, bbox[3] * scale_y - ] - - # 调整关键点坐标以适应原始图像大小 - kpts_scaled = kpts.xy[0].cpu().numpy() * np.array([scale_x, scale_y]) - - formatted_results.append({ - "bbox": bbox_scaled, - "confidence": box.conf.item(), - "keypoints": kpts_scaled.tolist() - }) return formatted_results def draw_results(self, frame, results): annotated_frame = frame.copy() for r in results: bbox = r["bbox"] - keypoints = r["keypoints"] - # 绘制边界框 cv2.rectangle(annotated_frame, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), (0, 255, 0), 2) - # 绘制关键点 - for kp in keypoints: - cv2.circle(annotated_frame, - (int(kp[0]), int(kp[1])), - 5, (255, 0, 0), -1) + # 如果有关键点信息,则绘制关键点 + if "keypoints" in r: + for kp in r["keypoints"]: + cv2.circle(annotated_frame, + (int(kp[0]), int(kp[1])), + 5, (255, 0, 0), -1) return annotated_frame @@ -217,15 +219,19 @@ def process_task(): result_path = os.path.join(RESULT_DIR, result_filename) cv2.imwrite(result_path, annotated_img) - redis_client.hmset(f"face_result:{task_id}", { - "result": json.dumps(json_results), - "result_file": result_filename - }) - main_redis_client.hmset(f"task:{task_id}", { - "status": "completed", - "result_type": "face", - "result_key": f"face_result:{task_id}" - }) + redis_client.hset(f"face_result:{task_id}", + mapping={ + "result": json.dumps(json_results), + "result_file": result_filename + } + ) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "completed", + "result_type": "face", + "result_key": f"face_result:{task_id}" + } + ) print(f"图像 {filename} 处理完成,结果已保存") else: print(f"图像 {filename} 处理失败") @@ -235,25 +241,31 @@ def process_task(): json_results = process_video(file_path) if json_results: result_filename = f"face_{filename}" - redis_client.hmset(f"face_result:{task_id}", { - "result": json.dumps(json_results), - "result_file": result_filename - }) - main_redis_client.hmset(f"task:{task_id}", { - "status": "completed", - "result_type": "face", - "result_key": f"face_result:{task_id}" - }) + redis_client.hset(f"face_result:{task_id}", + mapping={ + "result": json.dumps(json_results), + "result_file": result_filename + } + ) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "completed", + "result_type": "face", + "result_key": f"face_result:{task_id}" + } + ) print(f"视频 {filename} 处理完成,结果已保存") else: print(f"视频 {filename} 处理失败") main_redis_client.hset(f"task:{task_id}", "status", "failed") except Exception as e: print(f"处理任务 {task_id} 时出错: {str(e)}") - main_redis_client.hmset(f"task:{task_id}", { - "status": "failed", - "error": str(e) - }) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "failed", + "error": str(e) + } + ) print(f"任务 {task_id} 处理完毕,等待下一个Kafka消息...") def listen_redis_changes(): diff --git a/api/fall.py b/api/fall.py index 691f62d..2346de0 100644 --- a/api/fall.py +++ b/api/fall.py @@ -8,21 +8,17 @@ import json from kafka import KafkaConsumer import threading import redis -from config import * +from config import ( + KAFKA_BROKER, KAFKA_GROUP_ID_PREFIX, WORKER_CONFIGS, + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIN_REDIS_DB, + UPLOAD_DIR, RESULT_DIR, FALL_MODEL_PATH +) # 配置 MODEL_PATH = FALL_MODEL_PATH -KAFKA_BROKER = KAFKA_BROKER KAFKA_TOPIC = WORKER_CONFIGS["fall"]["kafka_topic"] KAFKA_GROUP_ID = f"fall_{KAFKA_GROUP_ID_PREFIX}" -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD REDIS_DB = WORKER_CONFIGS["fall"]["redis_db"] # Worker使用的Redis DB -MAIN_REDIS_DB = MAIN_REDIS_DB # 主Redis DB - -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR # 确保目录存在 os.makedirs(UPLOAD_DIR, exist_ok=True) diff --git a/api/media.py b/api/media.py index 2385c8f..fe29e39 100644 --- a/api/media.py +++ b/api/media.py @@ -11,21 +11,17 @@ import torch import mediapipe as mp from mediapipe.tasks import python from mediapipe.tasks.python import vision -from config import * +from config import ( + KAFKA_BROKER, KAFKA_GROUP_ID_PREFIX, WORKER_CONFIGS, + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIN_REDIS_DB, + UPLOAD_DIR, RESULT_DIR, MEDIAPIPE_MODEL_PATH +) # 配置 MODEL_PATH = MEDIAPIPE_MODEL_PATH -KAFKA_BROKER = KAFKA_BROKER KAFKA_TOPIC = WORKER_CONFIGS["mediapipe"]["kafka_topic"] KAFKA_GROUP_ID = f"mediapipe_{KAFKA_GROUP_ID_PREFIX}" -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD REDIS_DB = WORKER_CONFIGS["mediapipe"]["redis_db"] # Worker使用的Redis DB -MAIN_REDIS_DB = MAIN_REDIS_DB # 主Redis DB - -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR # Ensure directories exist diff --git a/api/pose.py b/api/pose.py index 42f71d9..dd3e648 100644 --- a/api/pose.py +++ b/api/pose.py @@ -9,22 +9,18 @@ from kafka import KafkaConsumer import threading import redis import torch -from config import * +from config import ( + KAFKA_BROKER, KAFKA_GROUP_ID_PREFIX, WORKER_CONFIGS, + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIN_REDIS_DB, + UPLOAD_DIR, RESULT_DIR, POSE_MODEL_PATH,CUDA_DEVICE_1 +) torch.cuda.set_device(CUDA_DEVICE_1) # 配置 MODEL_PATH = POSE_MODEL_PATH -KAFKA_BROKER = KAFKA_BROKER KAFKA_TOPIC = WORKER_CONFIGS["pose"]["kafka_topic"] KAFKA_GROUP_ID = f"pose_{KAFKA_GROUP_ID_PREFIX}" -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD REDIS_DB = WORKER_CONFIGS["pose"]["redis_db"] # Worker使用的Redis DB -MAIN_REDIS_DB = MAIN_REDIS_DB # 主Redis DB - -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR # 确保目录存在 os.makedirs(UPLOAD_DIR, exist_ok=True) @@ -219,15 +215,19 @@ def process_task(): result_path = os.path.join(RESULT_DIR, result_filename) cv2.imwrite(result_path, annotated_img) - redis_client.hmset(f"pose_result:{task_id}", { - "result": json.dumps(json_results), - "result_file": result_filename - }) - main_redis_client.hmset(f"task:{task_id}", { - "status": "completed", - "result_type": "pose", - "result_key": f"pose_result:{task_id}" - }) + redis_client.hset(f"pose_result:{task_id}", + mapping={ + "result": json.dumps(json_results), + "result_file": result_filename + } + ) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "completed", + "result_type": "pose", + "result_key": f"pose_result:{task_id}" + } + ) print(f"图像 {filename} 处理完成,结果已保存") else: print(f"图像 {filename} 处理失败") @@ -237,22 +237,26 @@ def process_task(): json_results = process_video(file_path) if json_results: result_filename = f"pose_{filename}" - redis_client.hmset(f"pose_result:{task_id}", { - "result": json.dumps(json_results), - "result_file": result_filename - }) - main_redis_client.hmset(f"task:{task_id}", { - "status": "completed", - "result_type": "pose", - "result_key": f"pose_result:{task_id}" - }) + redis_client.hset(f"pose_result:{task_id}", + mapping={ + "result": json.dumps(json_results), + "result_file": result_filename + } + ) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "completed", + "result_type": "pose", + "result_key": f"pose_result:{task_id}" + } + ) print(f"视频 {filename} 处理完成,结果已保存") else: print(f"视频 {filename} 处理失败") main_redis_client.hset(f"task:{task_id}", "status", "failed") except Exception as e: print(f"处理任务 {task_id} 时出错: {str(e)}") - main_redis_client.hmset(f"task:{task_id}", { + main_redis_client.hset(f"task:{task_id}", { "status": "failed", "error": str(e) }) diff --git a/api/qwenvl_analyze.py b/api/qwenvl_analyze.py index 95823fe..5ad3458 100644 --- a/api/qwenvl_analyze.py +++ b/api/qwenvl_analyze.py @@ -1,9 +1,8 @@ import os import json -import uuid from datetime import datetime, timedelta from kafka import KafkaConsumer -from transformers import Qwen2VLForConditionalGeneration, AutoProcessor +from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor from qwen_vl_utils import process_vision_info from decord import VideoReader, cpu from PIL import Image @@ -12,22 +11,20 @@ from redis import Redis import io import re import threading -from config import * +import torch +from config import ( + KAFKA_BROKER, KAFKA_GROUP_ID_PREFIX, WORKER_CONFIGS, + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIN_REDIS_DB, + UPLOAD_DIR, RESULT_DIR, QWEN_MODEL_PATH +) # 配置 MODEL_PATH = QWEN_MODEL_PATH -KAFKA_BROKER = KAFKA_BROKER KAFKA_TOPIC = WORKER_CONFIGS["qwenvl_analyze"]["kafka_topic"] KAFKA_GROUP_ID = f"qwenvl_analyze_{KAFKA_GROUP_ID_PREFIX}" -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD REDIS_DB = WORKER_CONFIGS["qwenvl_analyze"]["redis_db"] # Worker使用的Redis DB -MAIN_REDIS_DB = MAIN_REDIS_DB # 主Redis DB -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR # 确保目录存在 os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(RESULT_DIR, exist_ok=True) @@ -59,13 +56,13 @@ main_redis_client = Redis( # 初始化模型 -model = Qwen2VLForConditionalGeneration.from_pretrained( - MODEL_PATH, torch_dtype="auto", device_map="cuda:0" +model = Qwen2_5_VLForConditionalGeneration.from_pretrained( + MODEL_PATH, torch_dtype=torch.bfloat16, device_map="cuda:2" ) min_pixels = 128*28*28 max_pixels = 512*28*28 -processor = AutoProcessor.from_pretrained(MODEL_PATH, min_pixels=min_pixels, max_pixels=max_pixels) +processor = AutoProcessor.from_pretrained(MODEL_PATH, min_pixels=min_pixels, max_pixels=max_pixels, use_fast=True) class MediaAnalysisSystem: def __init__(self, model, processor): @@ -136,7 +133,7 @@ class MediaAnalysisSystem: padding=True, return_tensors="pt", ) - inputs = inputs.to('cuda:0') + inputs = inputs.to('cuda:2') generated_ids = self.model.generate(**inputs, max_new_tokens=2048) generated_ids_trimmed = [ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) diff --git a/api/qwenvl_scene.py b/api/qwenvl_scene.py index c5f594d..a9147ff 100644 --- a/api/qwenvl_scene.py +++ b/api/qwenvl_scene.py @@ -1,9 +1,8 @@ import os import json -import uuid from datetime import datetime, timedelta from kafka import KafkaConsumer -from transformers import Qwen2VLForConditionalGeneration, AutoProcessor +from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor from qwen_vl_utils import process_vision_info from decord import VideoReader, cpu from PIL import Image @@ -12,22 +11,20 @@ from redis import Redis import io import re import threading -from config import * +import torch +from config import ( + KAFKA_BROKER, KAFKA_GROUP_ID_PREFIX, WORKER_CONFIGS, + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIN_REDIS_DB, + UPLOAD_DIR, RESULT_DIR, QWEN_MODEL_PATH +) # 配置 MODEL_PATH = QWEN_MODEL_PATH -KAFKA_BROKER = KAFKA_BROKER KAFKA_TOPIC = WORKER_CONFIGS["qwenvl"]["kafka_topic"] KAFKA_GROUP_ID = f"qwenvl_{KAFKA_GROUP_ID_PREFIX}" -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD REDIS_DB = WORKER_CONFIGS["qwenvl"]["redis_db"] # Worker使用的Redis DB -MAIN_REDIS_DB = MAIN_REDIS_DB # 主Redis DB -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR # 确保目录存在 os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(RESULT_DIR, exist_ok=True) @@ -59,13 +56,13 @@ main_redis_client = Redis( # 初始化模型 -model = Qwen2VLForConditionalGeneration.from_pretrained( - MODEL_PATH, torch_dtype="auto", device_map="cuda:1" +model = Qwen2_5_VLForConditionalGeneration.from_pretrained( + MODEL_PATH, torch_dtype=torch.bfloat16, device_map="cuda:3" ) min_pixels = 128*28*28 max_pixels = 512*28*28 -processor = AutoProcessor.from_pretrained(MODEL_PATH, min_pixels=min_pixels, max_pixels=max_pixels) +processor = AutoProcessor.from_pretrained(MODEL_PATH,min_pixels=min_pixels, max_pixels=max_pixels, use_fast=True) class MediaAnalysisSystem: def __init__(self, model, processor): @@ -137,7 +134,7 @@ class MediaAnalysisSystem: padding=True, return_tensors="pt", ) - inputs = inputs.to('cuda:1') + inputs = inputs.to('cuda:3') generated_ids = self.model.generate(**inputs, max_new_tokens=2048) generated_ids_trimmed = [ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..b6fe906 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,16 @@ +fastapi +uvicorn +python-multipart +kafka-python==2.0.2 +six>=1.10.0 # 添加six包来解决kafka依赖问题 +redis +python-dotenv +requests +pydantic[email] +pydub +httpx +sqlalchemy +passlib[bcrypt] +pymysql +python-jose[cryptography] +Pillow # 添加用于图片处理 \ No newline at end of file diff --git a/api/start_services.sh b/api/start_services.sh new file mode 100755 index 0000000..5913267 --- /dev/null +++ b/api/start_services.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# 设置工作目录 +cd "$(dirname "$0")" + +# 创建日志目录 +mkdir -p logs + +# 定义要启动的服务 +services=( + "yolo.py" + "pose.py" + "qwenvl_scene.py" + "qwenvl_analyze.py" + "cpm_scene.py" + "cpm_analyze.py" + "fall.py" + "face.py" + "media.py" + "compare.py" +) + +# 启动所有服务 +for service in "${services[@]}"; do + echo "启动 $service..." + # 使用screen创建新的会话并运行Python服务 + screen_name="${service%.py}" + screen -dmS "$screen_name" bash -c "python3 $service > logs/${screen_name}.log 2>&1" + # 等待几秒钟,确保服务正常启动 + sleep 2 +done + +echo "所有服务已启动,请检查logs目录下的日志文件" +echo "使用 'screen -ls' 查看所有screen会话" +echo "使用 'screen -r [会话名]' 连接到特定会话" +echo "使用 'cat logs/*.log' 查看日志" \ No newline at end of file diff --git a/api/stop.sh b/api/stop.sh new file mode 100755 index 0000000..8af70bc --- /dev/null +++ b/api/stop.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# 获取所有screen会话并关闭它们 +screen -ls | grep Detached | cut -d. -f1 | awk '{print $1}' | while read pid +do + echo "正在终止screen会话: $pid" + screen -S $pid -X quit +done + +echo "所有screen会话已终止" + +# 为了以防万一,也可以直接杀死相关进程 +for prog in compare media face fall cpm_analyze cpm_scene pose yolo +do + echo "正在检查并终止 $prog 相关进程" + pkill -f $prog +done + +echo "清理完成" \ No newline at end of file diff --git a/api/yolo.py b/api/yolo.py index 947523c..6afb499 100644 --- a/api/yolo.py +++ b/api/yolo.py @@ -8,24 +8,20 @@ import json from kafka import KafkaConsumer import threading import redis -from config import * +from config import ( + KAFKA_BROKER, KAFKA_GROUP_ID_PREFIX, WORKER_CONFIGS, + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIN_REDIS_DB, + UPLOAD_DIR, RESULT_DIR, YOLO_MODEL_PATH +) # 配置 MODEL_PATH = YOLO_MODEL_PATH -KAFKA_BROKER = KAFKA_BROKER KAFKA_TOPIC = WORKER_CONFIGS["yolo"]["kafka_topic"] KAFKA_GROUP_ID = f"yolo_{KAFKA_GROUP_ID_PREFIX}" -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD REDIS_DB = WORKER_CONFIGS["yolo"]["redis_db"] # Worker使用的Redis DB -MAIN_REDIS_DB = MAIN_REDIS_DB # 主Redis DB - -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR - + # 确保目录存在 os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(RESULT_DIR, exist_ok=True) @@ -208,15 +204,19 @@ def process_task(): result_path = os.path.join(RESULT_DIR, result_filename) cv2.imwrite(result_path, annotated_img) - redis_client.hmset(f"yolo_result:{task_id}", { - "result": json.dumps(json_results), - "result_file": result_filename - }) - main_redis_client.hmset(f"task:{task_id}", { - "status": "completed", - "result_type": "yolo", - "result_key": f"yolo_result:{task_id}" - }) + redis_client.hset(f"yolo_result:{task_id}", + mapping={ + "result": json.dumps(json_results), + "result_file": result_filename + } + ) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "completed", + "result_type": "yolo", + "result_key": f"yolo_result:{task_id}" + } + ) print(f"图像 {filename} 处理完成,结果已保存") else: print(f"图像 {filename} 处理失败") @@ -226,25 +226,31 @@ def process_task(): json_results = process_video(file_path) if json_results: result_filename = f"yolo_{filename}" - redis_client.hmset(f"yolo_result:{task_id}", { - "result": json.dumps(json_results), - "result_file": result_filename - }) - main_redis_client.hmset(f"task:{task_id}", { - "status": "completed", - "result_type": "yolo", - "result_key": f"yolo_result:{task_id}" - }) + redis_client.hset(f"yolo_result:{task_id}", + mapping={ + "result": json.dumps(json_results), + "result_file": result_filename + } + ) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "completed", + "result_type": "yolo", + "result_key": f"yolo_result:{task_id}" + } + ) print(f"视频 {filename} 处理完成,结果已保存") else: print(f"视频 {filename} 处理失败") main_redis_client.hset(f"task:{task_id}", "status", "failed") except Exception as e: print(f"处理任务 {task_id} 时出错: {str(e)}") - main_redis_client.hmset(f"task:{task_id}", { - "status": "failed", - "error": str(e) - }) + main_redis_client.hset(f"task:{task_id}", + mapping={ + "status": "failed", + "error": str(e) + } + ) print(f"任务 {task_id} 处理完毕,等待下一个Kafka消息...") diff --git a/api_chat/.env b/api_chat/.env index d4f73ec..491f3bf 100644 --- a/api_chat/.env +++ b/api_chat/.env @@ -1,28 +1,27 @@ # Kafka 配置 -KAFKA_BROKER=222.186.10.253:9092 +KAFKA_BROKER=222.186.20.67:9092 KAFKA_ASR_TOPIC=asr KAFKA_CHAT_TOPIC=chat KAFKA_TTS_TOPIC=tts # Redis 配置 -REDIS_HOST=150.158.144.159 -REDIS_PORT=13003 -REDIS_ASR_DB=12 -REDIS_CHAT_DB=13 -REDIS_TTS_DB=14 +REDIS_HOST=222.186.20.67 +REDIS_PORT=6379 +REDIS_ASR_DB=43 +REDIS_CHAT_DB=44 +REDIS_TTS_DB=45 REDIS_PASSWORD=Obscura@2024 -REDIS_API_DB=2 -REDIS_API_USAGE_DB=3 -REDIS_TASK_DB=11 -REDIS_SESSION_DB=63 +REDIS_API_DB=31 +REDIS_API_USAGE_DB=32 +REDIS_TASK_DB=46 +REDIS_SESSION_DB=47 + +REDIS_SESSION_DB_ZH=48 +REDIS_SESSION_DB_EN=49 +REDIS_SESSION_DB_KO=50 -REDIS_SESSION_DB_ZH=63 -REDIS_SESSION_DB_EN=62 -REDIS_SESSION_DB_KO=61 -# CORS 配置 -# ALLOWED_ORIGINS=https://beta.obscura.work # GPT-SoVITS 配置 @@ -76,19 +75,21 @@ XUZHIYUAN_REF_AUDIO=sample/xuzhiyuan.wav XUZHIYUAN_REF_TEXT=sample/xuzhiyuan.txt -REDIS_GIRL_DB = 15 -REDIS_WOMAN_DB = 16 -REDIS_MAN_DB = 17 -REDIS_LEIJUN_DB = 18 -REDIS_DUFU_DB = 19 -REDIS_HEJIONG_DB = 20 -REDIS_MAHUATENG_DB = 21 -REDIS_LIDAN_DB = 22 -REDIS_DABING_DB = 23 -REDIS_LUOXIANG_DB = 24 -REDIS_XUZHIYUAN_DB = 25 -REDIS_YUHUA_DB = 26 -REDIS_LIUZHENYUN_DB = 27 - +REDIS_GIRL_DB = 51 +REDIS_WOMAN_DB = 52 +REDIS_MAN_DB = 53 +REDIS_LEIJUN_DB = 54 +REDIS_DUFU_DB = 55 +REDIS_HEJIONG_DB = 56 +REDIS_MAHUATENG_DB = 57 +REDIS_LIDAN_DB = 58 +REDIS_DABING_DB = 59 +REDIS_LUOXIANG_DB = 60 +REDIS_XUZHIYUAN_DB = 61 +REDIS_YUHUA_DB = 62 +REDIS_LIUZHENYUN_DB = 63 +# Ollama API配置 - 多个地址用逗号分隔 +OLLAMA_URLS=http://222.186.20.67:11435,http://222.186.20.67:11436,http://222.186.20.67:11437,http://222.186.20.67:11438,http://222.186.20.67:11439,http://222.186.20.67:11440,http://222.186.20.67:11441 +OLLAMA_TIMEOUT=10 # API请求超时时间(秒) \ No newline at end of file diff --git a/api_chat/GPT_SoVITS/.gitkeep b/api_chat/GPT_SoVITS/.gitkeep old mode 100644 new mode 100755 diff --git a/api_chat/OpenBMB/.gitkeep b/api_chat/OpenBMB/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/api_chat/asr.py b/api_chat/asr.py index c07d8ec..6a1164c 100644 --- a/api_chat/asr.py +++ b/api_chat/asr.py @@ -6,6 +6,9 @@ from dotenv import load_dotenv from kafka import KafkaConsumer import asyncio +# 在导入其他库之前设置 +os.environ["TOKENIZERS_PARALLELISM"] = "false" + # 设置要使用的GPU ID GPU_ID = 1 # 修改这个值来选择要使用的GPU @@ -16,7 +19,7 @@ os.environ["CUDA_VISIBLE_DEVICES"] = str(GPU_ID) load_dotenv() print("正在加载Whisper模型...") -model = whisper.load_model("large-v3") +model = whisper.load_model("small") print("Whisper模型加载完成。") # Kafka配置 diff --git a/api_chat/before/asr.py b/api_chat/before/asr.py deleted file mode 100644 index db0f524..0000000 --- a/api_chat/before/asr.py +++ /dev/null @@ -1,115 +0,0 @@ -import whisper -import os -import json -import redis -from dotenv import load_dotenv -from kafka import KafkaConsumer -import threading - -# 设置要使用的GPU ID -GPU_ID = 1 # 修改这个值来选择要使用的GPU - -# 设置CUDA_VISIBLE_DEVICES环境变量 -os.environ["CUDA_VISIBLE_DEVICES"] = str(GPU_ID) - -# 加载环境变量 -load_dotenv() - -print("正在加载Whisper模型...") -model = whisper.load_model("large-v3") -print("Whisper模型加载完成。") - -# Kafka配置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TOPIC = os.getenv('KAFKA_ASR_TOPIC') - -# Redis配置 -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_ASR_DB = int(os.getenv('REDIS_ASR_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') -REDIS_TASK_DB = int(os.getenv('REDIS_TASK_DB')) - -# 创建Redis客户端 -redis_asr_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_ASR_DB, - password=REDIS_PASSWORD -) - -redis_task_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_TASK_DB, - password=REDIS_PASSWORD -) - -def process_audio(file_path: str, client_id: str, cache_key: str): - try: - # 设置任务状态为 "processing" - redis_task_client.set(f"task_status:{cache_key}", "processing") - - result = model.transcribe(file_path) - transcription = result['text'] - - print(f"处理了文件: {file_path}") - print(f"转录结果: {transcription}") - - # 将结果存入Redis缓存 - redis_asr_client.setex(cache_key, 3600, transcription) # 缓存1小时 - - # 发布结果到Redis频道 - result_data = { - 'client_id': client_id, - 'transcription': transcription - } - redis_asr_client.publish('asr_results', json.dumps(result_data)) - - # 设置任务状态为 "completed" - redis_task_client.set(f"task_status:{cache_key}", "completed") - - # 清理临时文件 - os.remove(file_path) - - except Exception as e: - print(f"处理音频文件时发生错误: {str(e)}") - # 设置任务状态为 "error" - redis_task_client.set(f"task_status:{cache_key}", "error") - -def kafka_consumer(): - consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - group_id='asr_group', - auto_offset_reset='earliest', - enable_auto_commit=True - ) - - print(f"ASR消费者已启动") - - for message in consumer: - try: - task = message.value - file_path = task.get('file_path') - task_id = task.get('task_id') - status = task.get('status') - - if not file_path or not task_id or status != 'queued': - print(f"收到无效任务: {task}") - continue - - cache_key = f"asr:{task_id}" - client_id = task_id # 使用task_id作为client_id - - print(f"开始处理任务: {cache_key}") - process_audio(file_path, client_id, cache_key) - print(f"完成处理任务: {cache_key}") - - except Exception as e: - print(f"处理消息时发生错误: {str(e)}") - -if __name__ == "__main__": - print("启动Kafka消费者处理ASR请求...") - kafka_consumer() \ No newline at end of file diff --git a/api_chat/before/chat.py b/api_chat/before/chat.py deleted file mode 100644 index 6b9f264..0000000 --- a/api_chat/before/chat.py +++ /dev/null @@ -1,117 +0,0 @@ -from kafka import KafkaConsumer -import json -import asyncio -import redis -import os -from dotenv import load_dotenv -import requests -from concurrent.futures import ThreadPoolExecutor - -# 加载 .env 文件 -load_dotenv() - -# Kafka 设置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_CHAT_TOPIC = os.getenv('KAFKA_CHAT_TOPIC') -KAFKA_CONSUMER_GROUP = 'chat_group' -KAFKA_CONSUMER_NUM = 3 # 消费者数量 - -# Redis 设置 -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_CHAT_DB = int(os.getenv('REDIS_CHAT_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') -REDIS_TASK_DB = int(os.getenv('REDIS_TASK_DB')) - -# 创建Redis客户端 -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_CHAT_DB, - password=REDIS_PASSWORD -) - -redis_task_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_TASK_DB, - password=REDIS_PASSWORD -) - -DEFAULT_SYSTEM_PROMPT = "你是一个智能助手,请用尽可能简短、简洁、友好的方式回答问题。输入的所有内容都来自于语音识别输入,因此可能会出现各种错误,请尽可能猜测用户的意思" - -# 创建Kafka消费者 -def create_kafka_consumer(): - return KafkaConsumer( - KAFKA_CHAT_TOPIC, - bootstrap_servers=KAFKA_BROKER, - auto_offset_reset='latest', - enable_auto_commit=True, - group_id=KAFKA_CONSUMER_GROUP, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) - ) - -async def process_chat_request(chat_request): - try: - session_id = chat_request['session_id'] - query = chat_request['query'] - model = chat_request.get('model', 'qwen2.5:3b') - - # 设置任务状态为 "processing" - redis_task_client.set(f"task_status:{session_id}", "processing") - - # 从Redis获取历史记录 - history = json.loads(redis_client.get(session_id) or '[]') - - # 构建包含历史对话的完整提示 - full_prompt = DEFAULT_SYSTEM_PROMPT + "\n" - for past_query, past_response in history: - full_prompt += f"用户: {past_query}\n助手: {past_response}\n" - full_prompt += f"用户: {query}\n助手:" - - data = { - "model": model, - "prompt": full_prompt, - "stream": True, - "temperature": 0 - } - - response = requests.post("http://127.0.0.1:11434/api/generate", json=data, stream=True) - response.raise_for_status() - - text_output = "" - for line in response.iter_lines(): - if line: - json_data = json.loads(line) - if 'response' in json_data: - text_output += json_data['response'] - - # 更新历史记录 - history.append((query, text_output)) - redis_client.set(session_id, json.dumps(history)) - - # 设置任务状态为 "completed" - redis_task_client.set(f"task_status:{session_id}", "completed") - - print(f"处理完成 session {session_id}: {text_output}") - - except Exception as e: - print(f"处理 session {chat_request['session_id']} 时出错: {str(e)}") - # 设置任务状态为 "error" - redis_task_client.set(f"task_status:{chat_request['session_id']}", "error") - -def kafka_consumer_thread(consumer_id): - consumer = create_kafka_consumer() - print(f"消费者 {consumer_id} 已启动") - for message in consumer: - chat_request = message.value - asyncio.run(process_chat_request(chat_request)) - -def main(): - print("启动Kafka消费者处理聊天请求...") - with ThreadPoolExecutor(max_workers=KAFKA_CONSUMER_NUM) as executor: - for i in range(KAFKA_CONSUMER_NUM): - executor.submit(kafka_consumer_thread, i) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/api_chat/before/mini3_api.py b/api_chat/before/mini3_api.py deleted file mode 100644 index c21dc7f..0000000 --- a/api_chat/before/mini3_api.py +++ /dev/null @@ -1,182 +0,0 @@ -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel -import requests -import json -from typing import List, Tuple -from kafka import KafkaConsumer, TopicPartition -from concurrent.futures import ThreadPoolExecutor -import threading -import asyncio -import redis -import uuid -import logging -import uvicorn -from dotenv import load_dotenv -import os -import torch -from modelscope import AutoModelForCausalLM, AutoTokenizer - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -app = FastAPI() - -device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -torch.cuda.set_device(device) -print(f"Using device: {device}") - -# Load MiniCPM3-4B model -path = "/home/zydi/worker_chat/api/OpenBMB/MiniCPM3-4B" -tokenizer = AutoTokenizer.from_pretrained(path, trust_remote_code=True) -tokenizer.pad_token = tokenizer.eos_token -model = AutoModelForCausalLM.from_pretrained(path, torch_dtype=torch.bfloat16, device_map=device, trust_remote_code=True) - -# 加载 .env 文件 -load_dotenv() -# CORS 配置 -ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS').split(',') - -# 添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Kafka 设置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TOPIC = os.getenv('KAFKA_MINI3_TOPIC') -KAFKA_CONSUMER_GROUP = 'mini3_group' -KAFKA_CONSUMER_NUM = 1 - -# Redis 设置 -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_DB = int(os.getenv('REDIS_MINI3_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') - -# 创建Redis客户端 -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_DB, - password=REDIS_PASSWORD # 使用密码进行认证 -) -# 创建Kafka消费者 -def create_kafka_consumer(): - return KafkaConsumer( - bootstrap_servers=KAFKA_BROKER, - auto_offset_reset='earliest', - enable_auto_commit=True, - group_id=KAFKA_CONSUMER_GROUP, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) - ) - -# Kafka消费者函数 -def kafka_consumer(consumer, consumer_id): - # 获取消费者分配的分区 - consumer.subscribe([KAFKA_TOPIC]) - partitions = consumer.assignment() - - logger.info(f"消费者 {consumer_id} 被分配了以下分区: {[p.partition for p in partitions]}") - - for message in consumer: - partition = message.partition - offset = message.offset - chat_request = message.value # 直接使用 message.value,它已经是一个字典 - session_id = chat_request['session_id'] - query = chat_request['query'] - - logger.info(f"消费者 {consumer_id} 正在处理来自分区 {partition} 的消息:") - - asyncio.run(process_chat_request(chat_request)) - -# 启动Kafka消费者线程 -def start_kafka_consumers(num_consumers=KAFKA_CONSUMER_NUM): - consumers = [] - for i in range(num_consumers): - consumer = create_kafka_consumer() - consumer_thread = threading.Thread(target=kafka_consumer, args=(consumer, i), daemon=True) - consumer_thread.start() - consumers.append((consumer, consumer_thread)) - return consumers - -DEFAULT_SYSTEM_PROMPT = "你是一个智能助手,请用尽可能简短、简洁、友好的方式回答问题。输入的所有内容都来自于语音识别输入,因此可能会出现各种错误,请尽可能猜测用户的意思" - -class ChatRequest(BaseModel): - session_id: str - query: str - model: str = "minicpm3-4b" - -class ChatResponse(BaseModel): - response: str - history: List[Tuple[str, str]] - -# 处理聊天请求的异步函数 -async def process_chat_request(chat_request): - try: - response = await chat(ChatRequest(**chat_request)) - print(f"Processed message for session {chat_request['session_id']}: {response}") - except Exception as e: - print(f"Error processing message for session {chat_request['session_id']}: {str(e)}") - -@app.post("/mini3", response_model=ChatResponse) -async def chat(request: ChatRequest): - session_id = request.session_id - query = request.query - - # 从Redis获取历史记录 - history = json.loads(redis_client.get(session_id) or '[]') - - # 构建包含历史对话的完整提示 - messages = [{"role": "system", "content": DEFAULT_SYSTEM_PROMPT}] - for past_query, past_response in history: - messages.append({"role": "user", "content": past_query}) - messages.append({"role": "assistant", "content": past_response}) - messages.append({"role": "user", "content": query}) - - try: - model_inputs = tokenizer.apply_chat_template(messages, return_tensors="pt", add_generation_prompt=True) - - # 创建注意力掩码 - attention_mask = model_inputs.ne(tokenizer.pad_token_id).long() - - # 将输入移动到正确的设备(CPU或GPU) - model_inputs = model_inputs.to(device) - attention_mask = attention_mask.to(device) - - model_outputs = model.generate( - model_inputs, - attention_mask=attention_mask, - max_new_tokens=1024, - top_p=0.7, - temperature=0.7, - pad_token_id=tokenizer.eos_token_id, # 将pad_token_id设置为eos_token_id - do_sample=True - ) - - output_token_ids = model_outputs[0][len(model_inputs[0]):] - text_output = tokenizer.decode(output_token_ids, skip_special_tokens=True) - - # 更新历史记录 - history.append((query, text_output)) - redis_client.set(session_id, json.dumps(history)) - - return ChatResponse(response=text_output, history=history) - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/start_chat") -async def start_chat(): - session_id = str(uuid.uuid4()) - return {"session_id": session_id} - -if __name__ == '__main__': - # 启动Kafka消费者线程 - start_kafka_consumers() - - # 启动FastAPI服务器 - uvicorn.run(app, host="0.0.0.0", port=6003) \ No newline at end of file diff --git a/api_chat/before/mp4_to_wav.py b/api_chat/before/mp4_to_wav.py deleted file mode 100644 index 3838db7..0000000 --- a/api_chat/before/mp4_to_wav.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -from moviepy.editor import VideoFileClip - -def mp4_to_wav(input_file, output_file): - """ - 将MP4文件转换为WAV格式 - - :param input_file: 输入的MP4文件路径 - :param output_file: 输出的WAV文件路径 - """ - try: - # 加载视频文件 - video = VideoFileClip(input_file) - - # 提取音频 - audio = video.audio - - # 将音频写入WAV文件 - audio.write_audiofile(output_file) - - # 关闭视频和音频对象 - audio.close() - video.close() - - print(f"转换成功: {input_file} -> {output_file}") - except Exception as e: - print(f"转换失败: {input_file} - {str(e)}") - -def process_directory(directory): - """ - 处理目录中的所有MP4文件 - - :param directory: 包含MP4文件的目录路径 - """ - for filename in os.listdir(directory): - if filename.lower().endswith('.mp4'): - input_file = os.path.join(directory, filename) - output_file = os.path.splitext(input_file)[0] + ".wav" - mp4_to_wav(input_file, output_file) - -def main(): - # 获取输入路径 - input_path = input("请输入MP4文件或包含MP4文件的目录路径: ").strip() - - # 检查输入路径是否存在 - if not os.path.exists(input_path): - print("错误: 输入路径不存在") - return - - # 判断输入路径是文件还是目录 - if os.path.isfile(input_path): - if not input_path.lower().endswith('.mp4'): - print("错误: 输入文件不是MP4格式") - return - output_file = os.path.splitext(input_path)[0] + ".wav" - mp4_to_wav(input_path, output_file) - elif os.path.isdir(input_path): - process_directory(input_path) - else: - print("错误: 输入路径既不是文件也不是目录") - -if __name__ == "__main__": - main() diff --git a/api_chat/before/ollama_api.py b/api_chat/before/ollama_api.py deleted file mode 100644 index 991d071..0000000 --- a/api_chat/before/ollama_api.py +++ /dev/null @@ -1,170 +0,0 @@ -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel -import requests -import json -from typing import List, Tuple -from kafka import KafkaConsumer, TopicPartition -from concurrent.futures import ThreadPoolExecutor -import threading -import asyncio -import redis -import uuid -import logging -import uvicorn -from dotenv import load_dotenv -import os -import torch - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -app = FastAPI() - -device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -torch.cuda.set_device(device) -print(f"Using device: {device}") - -# 加载 .env 文件 -load_dotenv() -# CORS 配置 -ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS').split(',') - -# 添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Kafka 设置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TOPIC = os.getenv('KAFKA_CHAT_TOPIC') -KAFKA_CONSUMER_GROUP = 'chat_group' -KAFKA_CONSUMER_NUM = 1 - -# Redis 设置 -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_DB = int(os.getenv('REDIS_CHAT_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') - -# 创建Redis客户端 -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_DB, - password=REDIS_PASSWORD # 使用密码进行认证 -) -# 创建Kafka消费者 -def create_kafka_consumer(): - return KafkaConsumer( - bootstrap_servers=KAFKA_BROKER, - auto_offset_reset='earliest', - enable_auto_commit=True, - group_id=KAFKA_CONSUMER_GROUP, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) - ) - -# Kafka消费者函数 -def kafka_consumer(consumer, consumer_id): - # 获取消费者分配的分区 - consumer.subscribe([KAFKA_TOPIC]) - partitions = consumer.assignment() - - logger.info(f"消费者 {consumer_id} 被分配了以下分区: {[p.partition for p in partitions]}") - - for message in consumer: - partition = message.partition - offset = message.offset - chat_request = message.value # 直接使用 message.value,它已经是一个字典 - session_id = chat_request['session_id'] - query = chat_request['query'] - - logger.info(f"消费者 {consumer_id} 正在处理来自分区 {partition} 的消息:") - - asyncio.run(process_chat_request(chat_request)) - -# 启动Kafka消费者线程 -def start_kafka_consumers(num_consumers=KAFKA_CONSUMER_NUM): - consumers = [] - for i in range(num_consumers): - consumer = create_kafka_consumer() - consumer_thread = threading.Thread(target=kafka_consumer, args=(consumer, i), daemon=True) - consumer_thread.start() - consumers.append((consumer, consumer_thread)) - return consumers - -DEFAULT_SYSTEM_PROMPT = "你是一个智能助手,请用尽可能简短、简洁、友好的方式回答问题。输入的所有内容都来自于语音识别输入,因此可能会出现各种错误,请尽可能猜测用户的意思" - -class ChatRequest(BaseModel): - session_id: str - query: str - model: str = "qwen2.5:3b" - -class ChatResponse(BaseModel): - response: str - history: List[Tuple[str, str]] - -# 处理聊天请求的异步函数 -async def process_chat_request(chat_request): - try: - response = await chat(ChatRequest(**chat_request)) - print(f"Processed message for session {chat_request['session_id']}: {response}") - except Exception as e: - print(f"Error processing message for session {chat_request['session_id']}: {str(e)}") - -@app.post("/chat", response_model=ChatResponse) -async def chat(request: ChatRequest): - session_id = request.session_id - query = request.query - model = request.model - - # 从Redis获取历史记录 - history = json.loads(redis_client.get(session_id) or '[]') - - # 构建包含历史对话的完整提示 - full_prompt = DEFAULT_SYSTEM_PROMPT + "\n" - for past_query, past_response in history: - full_prompt += f"用户: {past_query}\n助手: {past_response}\n" - full_prompt += f"用户: {query}" - - data = { - "model": model, - "prompt": full_prompt, - "stream": True, - "temperature": 0 - } - - try: - response = requests.post("http://127.0.0.1:11434/api/generate", json=data, stream=True) - response.raise_for_status() - - text_output = "" - for line in response.iter_lines(): - if line: - json_data = json.loads(line) - if 'response' in json_data: - text_output += json_data['response'] - - # 更新历史记录 - history.append((query, text_output)) - redis_client.set(session_id, json.dumps(history)) - - return ChatResponse(response=text_output, history=history) - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/start_chat") -async def start_chat(): - session_id = str(uuid.uuid4()) - return {"session_id": session_id} - -if __name__ == '__main__': - # 启动Kafka消费者线程 - start_kafka_consumers() - - # 启动FastAPI服务器 - uvicorn.run(app, host="0.0.0.0", port=6001) \ No newline at end of file diff --git a/api_chat/before/ollamas.py b/api_chat/before/ollamas.py deleted file mode 100644 index 4d2db09..0000000 --- a/api_chat/before/ollamas.py +++ /dev/null @@ -1,136 +0,0 @@ -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from fastapi.middleware.cors import CORSMiddleware -import httpx -import json -import redis -from typing import List, Dict, Optional -import logging -import ollama -import uuid - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Redis连接 -redis_client = redis.Redis(host='222.186.10.253', port=6379, db=14, password="Obscura@2024") - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -class GenerateRequest(BaseModel): - model: Optional[str] = "qwen2.5:3b" - prompt: str - -class RawGenerateRequest(BaseModel): - model: Optional[str] = "qwen2.5:3b" - prompt: str - system_prompt: Optional[str] = None - stream: Optional[bool] = False - raw: Optional[bool] = False - format: Optional[str] = None - options: Optional[Dict] = None - -class GenerateResponse(BaseModel): - response: dict - request_id: str - -@app.post("/generate", response_model=GenerateResponse) -async def generate(request: GenerateRequest): - logger.info(f"收到请求: {request}") - - request_id = str(uuid.uuid4()) - - try: - response = ollama.chat(model=request.model, messages=[{"role": "user", "content": request.prompt}]) - full_response = response['message']['content'] - - request_data = { - "model": request.model, - "prompt": request.prompt, - "response": full_response - } - - redis_client.set(f"request:{request_id}", json.dumps(request_data)) - - response_data = { - "response": full_response, - "model": request.model - } - - return GenerateResponse(response=response_data, request_id=request_id) - - except Exception as e: - logger.error(f"发生错误: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/api/generate") -async def generate_without_history(request: RawGenerateRequest): - """ - 处理无历史记录的生成请求。 - - 参数: - - request: RawGenerateRequest对象,包含生成请求的所有参数。 - - 返回: - - 包含生成结果的字典。 - """ - try: - response = ollama.generate( - model=request.model, - prompt=request.prompt, - system=request.system_prompt, - format=request.format, - options=request.options, - stream=request.stream - ) - - response_data = { - "model": request.model, - "response": response['response'], - "done": True, - "context": response.get('context'), - "total_duration": response.get('total_duration'), - "load_duration": response.get('load_duration'), - "prompt_eval_count": response.get('prompt_eval_count'), - "prompt_eval_duration": response.get('prompt_eval_duration'), - "eval_count": response.get('eval_count'), - "eval_duration": response.get('eval_duration') - } - - request_id = str(uuid.uuid4()) - redis_client.set(f"request:{request_id}", json.dumps(response_data)) - - return response_data - - except Exception as e: - logger.error(f"发生未预期的错误: {e}") - logger.exception("详细错误信息:") - raise HTTPException(status_code=500, detail=f"处理Ollama请求时发生错误: {str(e)}") - -@app.get("/request/{request_id}", response_model=Dict) -async def get_request(request_id: str): - request_data = redis_client.get(f"request:{request_id}") - if request_data: - return json.loads(request_data) - raise HTTPException(status_code=404, detail="请求未找到") - -@app.get("/models") -async def list_models(): - return ollama.list() - -@app.get("/models/{model_name}") -async def show_model(model_name: str): - return ollama.show(model_name) - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7000) \ No newline at end of file diff --git a/api_chat/before/producer_chat_1.py b/api_chat/before/producer_chat_1.py deleted file mode 100644 index 907680a..0000000 --- a/api_chat/before/producer_chat_1.py +++ /dev/null @@ -1,319 +0,0 @@ -from fastapi import FastAPI, HTTPException, Depends, Security, File, UploadFile, WebSocket, WebSocketDisconnect -from fastapi.middleware.cors import CORSMiddleware -from fastapi.security import APIKeyHeader -from pydantic import BaseModel -from kafka import KafkaProducer -from redis import Redis -import os -import json -import uuid -from datetime import datetime, timezone -from dotenv import load_dotenv -import tempfile -import hashlib -import asyncio - - -# 加载 .env 文件 -load_dotenv() - -app = FastAPI() -v1_chat_app = FastAPI() -app.mount("/v1_chat", v1_chat_app) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') -REDIS_TTS_DB = int(os.getenv('REDIS_TTS_DB')) -REDIS_ASR_DB = int(os.getenv('REDIS_ASR_DB')) -REDIS_CHAT_DB = int(os.getenv('REDIS_CHAT_DB')) -REDIS_API_DB = int(os.getenv('REDIS_API_DB')) -REDIS_API_USAGE_DB = int(os.getenv('REDIS_API_USAGE_DB')) -REDIS_TASK_DB = int(os.getenv('REDIS_TASK_DB')) - -KAFKA_TTS_TOPIC = os.getenv('KAFKA_TTS_TOPIC') -KAFKA_ASR_TOPIC = os.getenv('KAFKA_ASR_TOPIC') -KAFKA_CHAT_TOPIC = os.getenv('KAFKA_CHAT_TOPIC') - -# 初始化 Kafka Producer -producer = KafkaProducer( - bootstrap_servers=[KAFKA_BROKER], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# 初始化 Redis -redis_tts_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_TTS_DB) -redis_asr_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_ASR_DB) -redis_chat_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_CHAT_DB) -redis_api_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_API_DB) -redis_api_usage_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_API_USAGE_DB) -redis_task_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_TASK_DB) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -def get_audio_hash(text): - return hashlib.md5(text.encode()).hexdigest() - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -async def process_request(api_key_info: dict, model_name: str, tokens_required: int, task_data: dict, kafka_topic: str): - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - await update_token_usage(api_key, tokens_required, model_name) - - # 发送任务到Kafka - producer.send(kafka_topic, task_data) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return { - "message": f"{model_name.upper()}请求已排队等待处理", - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - } - -class TTSRequest(BaseModel): - text: str - -class ChatRequest(BaseModel): - session_id: str - query: str - model: str = "qwen2.5:3b" - - -# 添加WebSocket连接管理 -class ConnectionManager: - def __init__(self): - self.active_connections = {} - - async def connect(self, websocket: WebSocket, client_id: str): - await websocket.accept() - self.active_connections[client_id] = websocket - - def disconnect(self, client_id: str): - self.active_connections.pop(client_id, None) - - async def send_message(self, message: str, client_id: str): - if client_id in self.active_connections: - await self.active_connections[client_id].send_text(message) - -manager = ConnectionManager() - - -@v1_chat_app.websocket("/ws/{client_id}") -async def websocket_endpoint(websocket: WebSocket, client_id: str): - await manager.connect(websocket, client_id) - try: - while True: - await websocket.receive_text() - except WebSocketDisconnect: - manager.disconnect(client_id) - -# 修改TTS请求处理函数 -@v1_chat_app.post("/tts") -async def tts_request(request: TTSRequest, api_key_info: dict = Depends(verify_api_key)): - task_id = str(uuid.uuid4()) - task_data = { - "task_id": task_id, - "text": request.text, - "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), - } - - redis_task_client.set(f"task_status:{task_id}", "queued") - - result = await process_request(api_key_info, "tts", 100, task_data, KAFKA_TTS_TOPIC) - result["task_id"] = task_id - - # 将任务ID存储到Redis,以便后续WebSocket通信使用 - redis_task_client.set(f"task_client:{task_id}", api_key_info['api_key']) - - return result - -# 修改ASR请求处理函数 -@v1_chat_app.post("/asr") -async def asr_request(audio: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - task_id = str(uuid.uuid4()) - - UPLOAD_DIR = "/obscura/task/audio_upload" - os.makedirs(UPLOAD_DIR, exist_ok=True) - file_path = os.path.join(UPLOAD_DIR, f"{task_id}.wav") - - with open(file_path, "wb") as temp_audio: - content = await audio.read() - temp_audio.write(content) - - task_data = { - 'file_path': file_path, - 'task_id': task_id, - 'status': 'queued' - } - - redis_task_client.set(f"task_status:{task_id}", "queued") - - result = await process_request(api_key_info, "asr", 100, task_data, KAFKA_ASR_TOPIC) - result["task_id"] = task_id - - # 将任务ID存储到Redis,以便后续WebSocket通信使用 - redis_task_client.set(f"task_client:{task_id}", api_key_info['api_key']) - - return result - -# 修改聊天请求处理函数 -@v1_chat_app.post("/chat") -async def chat_request(request: ChatRequest, api_key_info: dict = Depends(verify_api_key)): - task_id = str(uuid.uuid4()) - task_data = { - "task_id": task_id, - "session_id": request.session_id, - "query": request.query, - "model": request.model, - "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), - } - - redis_task_client.set(f"task_status:{task_id}", "queued") - - result = await process_request(api_key_info, "chat", 100, task_data, KAFKA_CHAT_TOPIC) - result["task_id"] = task_id - - # 将任务ID存储到Redis,以便后续WebSocket通信使用 - redis_task_client.set(f"task_client:{task_id}", api_key_info['api_key']) - - return result - -@v1_chat_app.get("/chat_result/{task_id}") -async def get_chat_result(task_id: str, api_key_info: dict = Depends(verify_api_key)): - # 从Redis任务数据库获取任务状态 - task_status = redis_task_client.get(f"task_status:{task_id}") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - # 从Redis聊天结果数据库获取聊天结果 - chat_result = redis_chat_client.get(task_id) - if chat_result: - result = json.loads(chat_result) - return { - "status": "completed", - "history": result # 直接返回整个历史记录 - } - return {"status": status} - - return {"status": "not_found"} - -@v1_chat_app.get("/tts_result/{task_id}") -async def get_tts_result(task_id: str, api_key_info: dict = Depends(verify_api_key)): - # 从Redis任务数据库获取任务状态 - task_status = redis_task_client.get(f"task_status:{task_id}") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - # 从Redis TTS结果数据库获取音频文件路径 - audio_info = redis_tts_client.get(task_id) - if audio_info: - audio_path = json.loads(audio_info)['path'] - return { - "status": "completed", - "audio_path": audio_path - } - return {"status": status} - - return {"status": "not_found"} - -@v1_chat_app.get("/asr_result/{task_id}") -async def get_asr_result(task_id: str, api_key_info: dict = Depends(verify_api_key)): - # 从Redis任务数据库获取任务状态 - task_status = redis_task_client.get(f"task_status:{task_id}") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - # 从Redis ASR结果数据库获取转录结果 - transcription = redis_asr_client.get(task_id) - if transcription: - return { - "status": "completed", - "transcription": transcription.decode('utf-8') - } - return {"status": status} - - return {"status": "not_found"} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8008) \ No newline at end of file diff --git a/api_chat/before/sovits_api.py b/api_chat/before/sovits_api.py deleted file mode 100644 index 1718c6c..0000000 --- a/api_chat/before/sovits_api.py +++ /dev/null @@ -1,180 +0,0 @@ -import os -import soundfile as sf -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse -from pydantic import BaseModel, Field -import uvicorn -import redis -import hashlib -import json -from kafka import KafkaProducer, KafkaConsumer -import threading -import time -from tools.i18n.i18n import I18nAuto -from GPT_SoVITS.inference_webui import change_gpt_weights, change_sovits_weights, get_tts_wav -from dotenv import load_dotenv -import os -import torch - -# 加载 .env 文件 -load_dotenv() -device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -print(f"Using device: {device}") - -# FastAPI configuration -app = FastAPI() -i18n = I18nAuto() - -# CORS configuration -ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS').split(',') - -# Redis configuration -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_DB = int(os.getenv('REDIS_TTS_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') - -# Kafka configuration -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TOPIC = os.getenv('KAFKA_TTS_TOPIC') -# KAFKA_GROUP_ID = 'tts_group' -KAFKA_CONSUMER_THREADS = 1 - -# TTS configuration -GPT_MODEL_PATH = os.getenv('GPT_MODEL_PATH') -SOVITS_MODEL_PATH = os.getenv('SOVITS_MODEL_PATH') -REF_AUDIO_PATH = os.getenv('REF_AUDIO_PATH') -REF_TEXT_PATH = os.getenv('REF_TEXT_PATH') -REF_LANGUAGE = os.getenv('REF_LANGUAGE') -TARGET_LANGUAGE = os.getenv('TARGET_LANGUAGE') -OUTPUT_PATH = os.getenv('OUTPUT_PATH') - -# Initialize FastAPI CORS -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Initialize Redis client -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_DB, - password=REDIS_PASSWORD -) - -# Initialize Kafka producer -kafka_producer = KafkaProducer(bootstrap_servers=KAFKA_BROKER) - -class TTSRequest(BaseModel): - text: str = Field(..., alias="text") - -def get_audio_hash(text): - return hashlib.md5(text.encode()).hexdigest() - -# Initialize models at startup -print("Initializing models...") -change_gpt_weights(gpt_path=GPT_MODEL_PATH) -change_sovits_weights(sovits_path=SOVITS_MODEL_PATH) - -# Read reference text -with open(REF_TEXT_PATH, 'r', encoding='utf-8') as file: - ref_text = file.read() - -print("Models initialized successfully.") - -def synthesize(target_text, output_path): - # Synthesize audio - with torch.cuda.device(device): - synthesis_result = get_tts_wav(ref_wav_path=REF_AUDIO_PATH, - prompt_text=ref_text, - prompt_language=i18n(REF_LANGUAGE), - text=target_text, - text_language=i18n(TARGET_LANGUAGE), top_p=1, temperature=1) - - result_list = list(synthesis_result) - - if result_list: - last_sampling_rate, last_audio_data = result_list[-1] - audio_hash = get_audio_hash(target_text) - output_wav_path = os.path.join(output_path, f"{audio_hash}.wav") - sf.write(output_wav_path, last_audio_data, last_sampling_rate) - return output_wav_path - else: - return None - -@app.post("/tts") -async def synthesize_audio(request: TTSRequest): - try: - print(f"Received TTS request: {request.dict()}") - target_text = request.text - audio_hash = get_audio_hash(target_text) - - # Check Redis cache - cached_audio = redis_client.get(audio_hash) - if cached_audio: - audio_info = json.loads(cached_audio) - return FileResponse(audio_info['path'], media_type="audio/wav") - - # Check file system - file_path = os.path.join(OUTPUT_PATH, f"{audio_hash}.wav") - if os.path.exists(file_path): - # Cache the file path in Redis - redis_client.set(audio_hash, json.dumps({"path": file_path})) - return FileResponse(file_path, media_type="audio/wav") - - # Send message to Kafka - kafka_producer.send(KAFKA_TOPIC, json.dumps({ - 'text': target_text, - 'audio_hash': audio_hash - }).encode('utf-8')) - - # Wait for the audio to be generated (you might want to implement a more sophisticated waiting mechanism) - for _ in range(60): # Wait for up to 30 seconds - if os.path.exists(file_path): - return FileResponse(file_path, media_type="audio/wav") - time.sleep(1) - - # If audio is not generated within the timeout - raise HTTPException(status_code=504, detail="Audio generation timed out") - except Exception as e: - print(f"Error processing TTS request: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/") -async def root(): - return {"message": "TTS API is running"} - -def kafka_consumer_thread(): - consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=KAFKA_BROKER, - # group_id=KAFKA_GROUP_ID, - auto_offset_reset='latest', - value_deserializer=lambda m: json.loads(m.decode('utf-8')) - ) - - for message in consumer: - target_text = message.value['text'] - audio_hash = message.value['audio_hash'] - - output_path = synthesize(target_text, OUTPUT_PATH) - - if output_path: - redis_client.set(audio_hash, json.dumps({"path": output_path})) - print(f"Audio synthesized successfully: {output_path}") - else: - print("Failed to synthesize audio") - -if __name__ == "__main__": - # Start Kafka consumer threads - torch.cuda.set_device(device) - for _ in range(KAFKA_CONSUMER_THREADS): - consumer_thread = threading.Thread(target=kafka_consumer_thread) - consumer_thread.start() - - uvicorn.run(app, host="0.0.0.0", port=6002) \ No newline at end of file diff --git a/api_chat/before/sovits_api_1.py b/api_chat/before/sovits_api_1.py deleted file mode 100644 index bdda30f..0000000 --- a/api_chat/before/sovits_api_1.py +++ /dev/null @@ -1,180 +0,0 @@ -import os -import soundfile as sf -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse -from pydantic import BaseModel, Field -import uvicorn -import redis -import hashlib -import json -from kafka import KafkaProducer, KafkaConsumer -import threading -import time -from tools.i18n.i18n import I18nAuto -from GPT_SoVITS.inference_webui import change_gpt_weights, change_sovits_weights, get_tts_wav -from dotenv import load_dotenv -import os -import torch - -# 加载 .env 文件 -load_dotenv() -device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -print(f"Using device: {device}") - -# FastAPI configuration -app = FastAPI() -i18n = I18nAuto() - -# CORS configuration -ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS').split(',') - -# Redis configuration -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_DB = int(os.getenv('REDIS_TTS_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') - -# Kafka configuration -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TOPIC = os.getenv('KAFKA_TTS_TOPIC') -# KAFKA_GROUP_ID = 'tts_group' -KAFKA_CONSUMER_THREADS = 1 - -# TTS configuration -GPT_MODEL_PATH = os.getenv('GPT_MODEL_PATH') -SOVITS_MODEL_PATH = os.getenv('SOVITS_MODEL_PATH') -REF_AUDIO_PATH = os.getenv('REF_AUDIO_KO_PATH') -REF_TEXT_PATH = os.getenv('REF_TEXT_KO_PATH') -REF_LANGUAGE = os.getenv('REF_KO_LANGUAGE') -TARGET_LANGUAGE = os.getenv('TARGET_LANGUAGE') -OUTPUT_PATH = os.getenv('OUTPUT_PATH') - -# Initialize FastAPI CORS -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Initialize Redis client -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_DB, - password=REDIS_PASSWORD -) - -# Initialize Kafka producer -kafka_producer = KafkaProducer(bootstrap_servers=KAFKA_BROKER) - -class TTSRequest(BaseModel): - text: str = Field(..., alias="text") - -def get_audio_hash(text): - return hashlib.md5(text.encode()).hexdigest() - -# Initialize models at startup -print("Initializing models...") -change_gpt_weights(gpt_path=GPT_MODEL_PATH) -change_sovits_weights(sovits_path=SOVITS_MODEL_PATH) - -# Read reference text -with open(REF_TEXT_PATH, 'r', encoding='utf-8') as file: - ref_text = file.read() - -print("Models initialized successfully.") - -def synthesize(target_text, output_path): - # Synthesize audio - with torch.cuda.device(device): - synthesis_result = get_tts_wav(ref_wav_path=REF_AUDIO_PATH, - prompt_text=ref_text, - prompt_language=i18n(REF_LANGUAGE), - text=target_text, - text_language=i18n(TARGET_LANGUAGE), top_p=1, temperature=1) - - result_list = list(synthesis_result) - - if result_list: - last_sampling_rate, last_audio_data = result_list[-1] - audio_hash = get_audio_hash(target_text) - output_wav_path = os.path.join(output_path, f"{audio_hash}.wav") - sf.write(output_wav_path, last_audio_data, last_sampling_rate) - return output_wav_path - else: - return None - -@app.post("/tts_ko") -async def synthesize_audio(request: TTSRequest): - try: - print(f"Received TTS request: {request.dict()}") - target_text = request.text - audio_hash = get_audio_hash(target_text) - - # Check Redis cache - cached_audio = redis_client.get(audio_hash) - if cached_audio: - audio_info = json.loads(cached_audio) - return FileResponse(audio_info['path'], media_type="audio/wav") - - # Check file system - file_path = os.path.join(OUTPUT_PATH, f"{audio_hash}.wav") - if os.path.exists(file_path): - # Cache the file path in Redis - redis_client.set(audio_hash, json.dumps({"path": file_path})) - return FileResponse(file_path, media_type="audio/wav") - - # Send message to Kafka - kafka_producer.send(KAFKA_TOPIC, json.dumps({ - 'text': target_text, - 'audio_hash': audio_hash - }).encode('utf-8')) - - # Wait for the audio to be generated (you might want to implement a more sophisticated waiting mechanism) - for _ in range(60): # Wait for up to 30 seconds - if os.path.exists(file_path): - return FileResponse(file_path, media_type="audio/wav") - time.sleep(1) - - # If audio is not generated within the timeout - raise HTTPException(status_code=504, detail="Audio generation timed out") - except Exception as e: - print(f"Error processing TTS request: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/") -async def root(): - return {"message": "TTS API is running"} - -def kafka_consumer_thread(): - consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=KAFKA_BROKER, - # group_id=KAFKA_GROUP_ID, - auto_offset_reset='latest', - value_deserializer=lambda m: json.loads(m.decode('utf-8')) - ) - - for message in consumer: - target_text = message.value['text'] - audio_hash = message.value['audio_hash'] - - output_path = synthesize(target_text, OUTPUT_PATH) - - if output_path: - redis_client.set(audio_hash, json.dumps({"path": output_path})) - print(f"Audio synthesized successfully: {output_path}") - else: - print("Failed to synthesize audio") - -if __name__ == "__main__": - # Start Kafka consumer threads - torch.cuda.set_device(device) - for _ in range(KAFKA_CONSUMER_THREADS): - consumer_thread = threading.Thread(target=kafka_consumer_thread) - consumer_thread.start() - - uvicorn.run(app, host="0.0.0.0", port=6003) \ No newline at end of file diff --git a/api_chat/before/tts1.py b/api_chat/before/tts1.py deleted file mode 100644 index 7bf9e69..0000000 --- a/api_chat/before/tts1.py +++ /dev/null @@ -1,176 +0,0 @@ -# 导入所需的库 -import os -import soundfile as sf -import redis -import hashlib -import json -from kafka import KafkaConsumer -from tools.i18n.i18n import I18nAuto -from GPT_SoVITS.inference_webui import change_gpt_weights, change_sovits_weights, get_tts_wav -from dotenv import load_dotenv -import torch - -""" -整体设计说明: -这个脚本实现了一个文本到语音(TTS)的服务。它使用Kafka作为消息队列接收TTS任务, -使用Redis存储任务状态和结果,并利用GPT-SoVITS模型进行语音合成。 -主要功能包括: -1. 初始化配置和模型 -2. 提供语音合成功能 -3. 监听Kafka消息并处理TTS任务 -4. 将合成结果存储到Redis并更新任务状态 -""" - -# 加载环境变量 -load_dotenv() - -# 设置GPU设备(如果可用) -device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -print(f"使用设备: {device}") - -# 从环境变量中读取Redis配置 -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_TTS_DB = int(os.getenv('REDIS_TTS_DB')) # DB 2用于存储TTS结果 -REDIS_TASK_DB = int(os.getenv('REDIS_TASK_DB')) # DB 3用于存储任务状态 -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') - -# 从环境变量中读取Kafka配置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TTS_TOPIC = os.getenv('KAFKA_TTS_TOPIC') - -# 从环境变量中读取TTS相关配置 -GPT_MODEL_PATH = os.getenv('GPT_MODEL_PATH') -SOVITS_MODEL_PATH = os.getenv('SOVITS_MODEL_PATH') -REF_AUDIO_PATH = os.getenv('REF_AUDIO_ZN_PATH') -REF_TEXT_PATH = os.getenv('REF_TEXT_ZN_PATH') -REF_LANGUAGE = os.getenv('REF_LANGUAGE') -TARGET_LANGUAGE = os.getenv('TARGET_LANGUAGE') -OUTPUT_PATH = os.getenv('OUTPUT_PATH') - -# 初始化Redis客户端 -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_TTS_DB, - password=REDIS_PASSWORD -) - -redis_task_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_TASK_DB, - password=REDIS_PASSWORD -) - -# 初始化国际化工具 -i18n = I18nAuto() - -def get_audio_hash(text): - """ - 生成文本的MD5哈希值,用作音频文件名的一部分 - - 参数: - text (str): 需要生成哈希的文本 - - 返回: - str: 文本的MD5哈希值 - """ - return hashlib.md5(text.encode()).hexdigest() - -# 初始化模型 -print("正在初始化模型...") -change_gpt_weights(gpt_path=GPT_MODEL_PATH) -change_sovits_weights(sovits_path=SOVITS_MODEL_PATH) - -# 读取参考文本 -with open(REF_TEXT_PATH, 'r', encoding='utf-8') as file: - ref_text = file.read() - -print("模型初始化成功。") - -def synthesize(target_text, output_wav_path): - """ - 使用GPT-SoVITS模型合成语音 - - 参数: - target_text (str): 需要合成语音的目标文本 - output_wav_path (str): 输出音频文件的路径 - - 返回: - str: 如果成功,返回输出音频文件的路径;如果失败,返回None - """ - with torch.cuda.device(device): - synthesis_result = get_tts_wav(ref_wav_path=REF_AUDIO_PATH, - prompt_text=ref_text, - prompt_language=i18n(REF_LANGUAGE), - text=target_text, - text_language=i18n(TARGET_LANGUAGE), top_p=1, temperature=1) - - result_list = list(synthesis_result) - - if result_list: - last_sampling_rate, last_audio_data = result_list[-1] - sf.write(output_wav_path, last_audio_data, last_sampling_rate) - return output_wav_path - else: - return None - -def kafka_consumer(): - """ - Kafka消费者函数,用于接收和处理TTS任务 - - 该函数会持续监听Kafka的TTS主题,接收任务并进行处理: - 1. 接收任务信息 - 2. 更新任务状态 - 3. 调用synthesize函数合成语音 - 4. 将结果保存到Redis - 5. 更新任务完成状态 - """ - consumer = KafkaConsumer( - KAFKA_TTS_TOPIC, - bootstrap_servers=KAFKA_BROKER, - auto_offset_reset='latest', - value_deserializer=lambda m: json.loads(m.decode('utf-8')) - ) - print(f"TTS消费者已启动") - for message in consumer: - try: - task_id = message.value['task_id'] - target_text = message.value['text'] - text_hash = message.value['text_hash'] - - # 更新任务状态为 "processing" - redis_task_client.set(f"task_status:tts:{task_id}", "processing") - - output_wav_path = os.path.join(OUTPUT_PATH, f"{text_hash}.wav") - - # 再次检查文件是否存在(以防在此期间被其他进程创建) - if not os.path.exists(output_wav_path): - output_path = synthesize(target_text, output_wav_path) - else: - output_path = output_wav_path - - if output_path: - # 将结果保存在 DB 2 - redis_client.set(f"tts:{task_id}", json.dumps({"path": output_path})) - print(f"音频合成成功: {output_path}") - - # 更新任务状态为 "completed" - redis_task_client.set(f"task_status:tts:{task_id}", "completed") - else: - print("音频合成失败") - - # 更新任务状态为 "failed" - redis_task_client.set(f"task_status:tts:{task_id}", "failed") - except Exception as e: - print(f"处理消息时出错: {str(e)}") - - # 更新任务状态为 "failed" - redis_task_client.set(f"task_status:tts:{task_id}", "failed") - -if __name__ == "__main__": - # 设置CUDA设备 - torch.cuda.set_device(device) - # 启动Kafka消费者 - kafka_consumer() \ No newline at end of file diff --git a/api_chat/before/wav_to_text.py b/api_chat/before/wav_to_text.py deleted file mode 100644 index 7dd8527..0000000 --- a/api_chat/before/wav_to_text.py +++ /dev/null @@ -1,68 +0,0 @@ -import os -import whisper -import argparse - -def transcribe_audio(model, audio_path): - """ - 使用Whisper模型转录音频文件 - - :param model: 加载的Whisper模型 - :param audio_path: 音频文件路径 - :return: 转录的文本 - """ - try: - result = model.transcribe(audio_path) - return result["text"] - except Exception as e: - print(f"转录失败 {audio_path}: {str(e)}") - return None - -def process_directory(directory, model): - """ - 处理目录中的所有WAV文件 - - :param directory: 包含WAV文件的目录路径 - :param model: 加载的Whisper模型 - """ - for filename in os.listdir(directory): - if filename.lower().endswith('.wav'): - input_file = os.path.join(directory, filename) - output_file = os.path.splitext(input_file)[0] + ".txt" - - print(f"正在处理: {input_file}") - transcription = transcribe_audio(model, input_file) - - if transcription: - with open(output_file, 'w', encoding='utf-8') as f: - f.write(transcription) - print(f"转录完成: {output_file}") - else: - print(f"转录失败: {input_file}") - -def main(): - parser = argparse.ArgumentParser(description="使用Whisper将WAV文件转换为文本") - parser.add_argument("input_path", help="输入的WAV文件或包含WAV文件的目录路径") - parser.add_argument("--model", default="small", choices=["tiny", "base", "small", "medium", "large", "large-v3"], help="Whisper模型大小") - args = parser.parse_args() - - print(f"正在加载Whisper模型 ({args.model})...") - model = whisper.load_model(args.model) - print("模型加载完成") - - if os.path.isfile(args.input_path): - if not args.input_path.lower().endswith('.wav'): - print("错误: 输入文件不是WAV格式") - return - output_file = os.path.splitext(args.input_path)[0] + ".txt" - transcription = transcribe_audio(model, args.input_path) - if transcription: - with open(output_file, 'w', encoding='utf-8') as f: - f.write(transcription) - print(f"转录完成: {output_file}") - elif os.path.isdir(args.input_path): - process_directory(args.input_path, model) - else: - print("错误: 输入路径既不是文件也不是目录") - -if __name__ == "__main__": - main() diff --git a/api_chat/before/weight.json b/api_chat/before/weight.json deleted file mode 100644 index 58ec624..0000000 --- a/api_chat/before/weight.json +++ /dev/null @@ -1 +0,0 @@ -{"GPT": {"v2": "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s1bert25hz-5kh-longer-epoch=12-step=369668.ckpt"}, "SoVITS": {"v2": "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s2G2333k.pth"}} \ No newline at end of file diff --git a/api_chat/before/whisper_api.py b/api_chat/before/whisper_api.py deleted file mode 100644 index cb221b0..0000000 --- a/api_chat/before/whisper_api.py +++ /dev/null @@ -1,186 +0,0 @@ -from fastapi import FastAPI, File, UploadFile, HTTPException, WebSocket -from fastapi.middleware.cors import CORSMiddleware -import whisper -import tempfile -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from starlette.websockets import WebSocketDisconnect -import json -import threading -import os -import uuid -import asyncio -import logging -import redis -from dotenv import load_dotenv - -# 设置要使用的GPU ID -GPU_ID = 1 # 修改这个值来选择要使用的GPU - -# 设置CUDA_VISIBLE_DEVICES环境变量 -os.environ["CUDA_VISIBLE_DEVICES"] = str(GPU_ID) - -# 设置日志 -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -app = FastAPI() -load_dotenv() - - -# CORS 配置 -ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS').split(',') - - -# 添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -print("正在加载Whisper模型...") -model = whisper.load_model("large-v3") -print("Whisper模型加载完成。") - -# Kafka配置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -KAFKA_TOPIC = os.getenv('KAFKA_ASR_TOPIC') - -# Redis配置 -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_DB = int(os.getenv('REDIS_ASR_DB')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') - -# 创建Redis客户端 -redis_client = redis.Redis( - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_DB, - password=REDIS_PASSWORD # 添加密码 -) - -# Kafka生产者 -producer = KafkaProducer( - bootstrap_servers=[KAFKA_BROKER], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# 存储WebSocket连接的字典 -active_connections = {} - -@app.websocket("/asr/ws/{client_id}") -async def websocket_endpoint(websocket: WebSocket, client_id: str): - await websocket.accept() - active_connections[client_id] = websocket - try: - while True: - try: - # 设置接收超时 - data = await asyncio.wait_for(websocket.receive_text(), timeout=30) - if data == "ping": - await websocket.send_text("pong") - else: - await websocket.send_text(f"收到消息: {data}") - except asyncio.TimeoutError: - try: - # 发送心跳 - await websocket.send_text("heartbeat") - except WebSocketDisconnect: - logger.info(f"客户端 {client_id} 断开连接") - break - except WebSocketDisconnect: - logger.info(f"客户端 {client_id} 断开连接") - except Exception as e: - logger.error(f"WebSocket错误: {e}") - finally: - if client_id in active_connections: - del active_connections[client_id] - - -@app.post("/asr") -async def transcribe(audio: UploadFile = File(...)): - if not audio: - raise HTTPException(status_code=400, detail="未提供音频文件") - - client_id = str(uuid.uuid4()) - - # 生成缓存键 - cache_key = f"asr:{audio.filename}:{client_id}" - - # 检查缓存 - cached_result = redis_client.get(cache_key) - if cached_result: - logger.info(f"缓存命中: {cache_key}") - return {"message": "从缓存获取转录结果", "transcription": cached_result.decode('utf-8'), "client_id": client_id} - - with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_audio: - content = await audio.read() - temp_audio.write(content) - temp_audio.flush() - - task = { - 'file_path': temp_audio.name, - 'client_id': client_id, - 'cache_key': cache_key - } - producer.send(KAFKA_TOPIC, value=task) - producer.flush() - - logger.info(f"发送任务到Kafka: {task}") - return {"message": "音频文件已接收并发送任务进行处理", "client_id": client_id} - -async def send_transcription(client_id: str, transcription: str): - if client_id in active_connections: - websocket = active_connections[client_id] - await websocket.send_json({"transcription": transcription}) - else: - logger.warning(f"客户端 {client_id} 的WebSocket连接不存在") - -def kafka_consumer(consumer_id): - consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - group_id='asr_group', - max_poll_interval_ms=300000 - ) - - for message in consumer: - try: - task = message.value - file_path = task.get('file_path') - client_id = task.get('client_id') - cache_key = task.get('cache_key') - - if not file_path or not client_id or not cache_key: - logger.error(f"消费者 {consumer_id} 收到无效任务: {task}") - consumer.commit() - continue - - result = model.transcribe(file_path) - - logger.info(f"消费者 {consumer_id} 处理了文件: {file_path}") - logger.info(f"转录结果: {result['text']}") - - # 将结果存入Redis缓存 - redis_client.setex(cache_key, 3600, result['text']) # 缓存1小时 - - asyncio.run(send_transcription(client_id, result['text'])) - - os.remove(file_path) - consumer.commit() - except Exception as e: - logger.error(f"消费者 {consumer_id} 处理消息时发生错误: {str(e)}") - -def start_consumers(num_consumers=1): - for i in range(num_consumers): - consumer_thread = threading.Thread(target=kafka_consumer, args=(i,)) - consumer_thread.start() - -if __name__ == '__main__': - start_consumers() - uvicorn.run(app, host="0.0.0.0", port=6000) \ No newline at end of file diff --git a/api_chat/chat.py b/api_chat/chat.py index 50bc293..a6ebbf6 100644 --- a/api_chat/chat.py +++ b/api_chat/chat.py @@ -23,6 +23,10 @@ REDIS_CHAT_DB = int(os.getenv('REDIS_CHAT_DB')) REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') REDIS_TASK_DB = int(os.getenv('REDIS_TASK_DB')) +OLLAMA_URL = os.getenv('OLLAMA_URL') +OLLAMA_URLS = os.getenv('OLLAMA_URLS', OLLAMA_URL).split(',') # 兼容旧配置 +OLLAMA_TIMEOUT = int(os.getenv('OLLAMA_TIMEOUT', 10)) + # 创建Redis客户端 redis_client = redis.Redis( host=REDIS_HOST, @@ -51,6 +55,21 @@ def create_kafka_consumer(): value_deserializer=lambda x: json.loads(x.decode('utf-8')) ) +async def try_ollama_request(url, data): + """尝试向单个 Ollama API 发送请求""" + try: + response = requests.post( + f"{url}/api/generate", + json=data, + stream=True, + timeout=OLLAMA_TIMEOUT + ) + response.raise_for_status() + return response + except Exception as e: + print(f"API {url} 请求失败: {str(e)}") + return None + async def process_chat_request(chat_request): try: task_id = chat_request['task_id'] @@ -77,9 +96,17 @@ async def process_chat_request(chat_request): "temperature": 0 } - response = requests.post("https://ffgregevrdcfyhtnhyudvr.myfastools.com/api/generate", json=data, stream=True) - response.raise_for_status() + # 尝试所有可用的 API 地址 + response = None + for url in OLLAMA_URLS: + response = await try_ollama_request(url, data) + if response is not None: + print(f"使用 API 地址: {url}") + break + if response is None: + raise Exception("所有 API 地址均不可用") + text_output = "" for line in response.iter_lines(): if line: @@ -105,6 +132,7 @@ async def process_chat_request(chat_request): # 设置任务状态为 "error" redis_task_client.set(f"chat:{task_id}:status", "error") redis_task_client.set(f"chat:{task_id}:error", str(e)) + def kafka_consumer_thread(consumer_id): consumer = create_kafka_consumer() print(f"消费者 {consumer_id} 已启动") diff --git a/api_chat/docs/.gitkeep b/api_chat/docs/.gitkeep old mode 100644 new mode 100755 diff --git a/api_chat/runtime/.gitkeep b/api_chat/runtime/.gitkeep old mode 100644 new mode 100755 diff --git a/api_chat/tools/.gitkeep b/api_chat/tools/.gitkeep old mode 100644 new mode 100755 diff --git a/api_chat/weight.json b/api_chat/weight.json new file mode 100644 index 0000000..8002545 --- /dev/null +++ b/api_chat/weight.json @@ -0,0 +1 @@ +{"GPT": {"v1": "GPT_SoVITS/pretrained_models/s1bert25hz-2kh-longer-epoch=68e-step=50232.ckpt", "v2": "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s1bert25hz-5kh-longer-epoch=12-step=369668.ckpt"}, "SoVITS": {"v1": "GPT_SoVITS/pretrained_models/s2G488k.pth", "v2": "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s2G2333k.pth"}} \ No newline at end of file diff --git a/api_history/OpenBMB/.gitkeep b/api_history/OpenBMB/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/api_history/function/config.yaml b/api_history/function/config.yaml deleted file mode 100644 index 319e651..0000000 --- a/api_history/function/config.yaml +++ /dev/null @@ -1,35 +0,0 @@ -kafka: - bootstrap_servers: - - "222.186.136.78:9092" - value_serializer: "json" - topics: - all_frames: - name: "pose-input" - num_consumers: 3 - ten_seconds: - name: "cpm-input" - num_consumers: 1 - input_topic: - name: "raw-data" - num_consumers: 3 - -redis: - host: "222.186.136.78" - port: 6379 - db: 0 - password: "Obscura@2024" - -minio: - endpoint: "api.obscura.work" - access_key: "00v3MtLtIAIkR3hkIuYR" - secret_key: "XfDeVe5bJjPU21NEYc023gzJVUTJzQqxsWHqIKMf" - secure: true - -mongodb: - uri: "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name: "minio_mongo" - -model: - pose-path: "worker_sys/function/yolov8n-pose.pt" - cpm-path: "worker_sys/OpenBMB/MiniCPM-V-2_6" - diff --git a/api_history/function/cpm.py b/api_history/function/cpm.py deleted file mode 100644 index f2fce55..0000000 --- a/api_history/function/cpm.py +++ /dev/null @@ -1,362 +0,0 @@ -import json -import io -from PIL import Image -import torch -from kafka import KafkaConsumer, KafkaProducer -from transformers import AutoModel, AutoTokenizer -import threading -import re -from datetime import datetime -import time -import base64 -import numpy as np -import cv2 -import redis -from redis import ConnectionPool -import yaml - -# 加载配置文件 -with open('worker_sys/function/config.yaml', 'r') as file: - config = yaml.safe_load(file) - -class JSONEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, datetime): - return obj.isoformat() - return json.JSONEncoder.default(self, obj) - -class ImageSequenceProcessor: - def __init__(self, model_dir): - self.model = AutoModel.from_pretrained(model_dir, trust_remote_code=True, - attn_implementation='sdpa', torch_dtype=torch.bfloat16).eval().cuda() - self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) - self.max_size = 512 # 设置最大尺寸 - def extract_time_from_key(self, key_name): - # 从key_name中提取时间信息 - time_str = key_name.split('_')[-2] + '_' + key_name.split('_')[-1].split('.')[0] - return datetime.strptime(time_str, "%Y%m%d_%H%M%S") - - def process_image_sequence(self, image_data, key_names): - frames = [] - image_times = [] - for i, (img, key_name) in enumerate(zip(image_data, key_names)): - try: - if isinstance(img, np.ndarray): - # 确保图像是 RGB 格式 - if img.shape[2] == 3: - frame = Image.fromarray(img) - else: - print(f"Unexpected number of channels for image {i}: {img.shape[2]}") - continue - else: - print(f"Unexpected data type for image {i}: {type(img)}") - continue - frames.append(frame) - image_times.append(self.extract_time_from_key(key_name)) - print(f"Successfully processed frame {i}") - except Exception as e: - print(f"Error processing frame {i}: {str(e)}") - continue - - if not frames: - raise ValueError("No valid frames were processed") - - # 修改时间范围格式 - start_time = min(image_times) - end_time = max(image_times) - time_range = { - 'start': start_time.strftime('%Y-%m-%d %H:%M'), - 'end': end_time.strftime('%Y-%m-%d %H:%M') - } - - # # 计算平均时间间隔(以分钟为单位) - # if len(image_times) > 1: - # sequence_period_seconds = (image_times[-1] - image_times[0]).total_seconds()/ (len(image_times) - 1) - # else: - # sequence_period_seconds = 0 - - total_duration = (end_time - start_time).total_seconds() - num_images = len(image_times) - if num_images > 1: - sequence_period_seconds = total_duration / (num_images - 1) - else: - sequence_period_seconds = 0 - - question = "Analyze these 3 images as if they were frames from a video. Describe the scene in detail in Chinese, including the setting, number of people, their actions, and any changes or movements observed across the frames." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - "time_range": time_range, - "sequence_period_seconds": sequence_period_seconds - } - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - # 环境提取 - environments = ["办公室", "室内", "室外", "会议室", "办公"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - # 改进的人数提取 - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - # 动作和互动提取 - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - # 物体和家具提取 - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -class ImageSequenceAnalysisSystem: - def __init__(self, model_dir): - self.image_processor = ImageSequenceProcessor(model_dir) - - def process_image_sequence(self, image_data, cache_keys): - print(f"Attempting to process sequence of {len(image_data)} images") - start_time = time.time() - try: - print("Processing new image sequence...") - for i, (img, cache_key) in enumerate(zip(image_data, cache_keys)): - print(f"Image {i} type: {type(img)}") - if isinstance(img, np.ndarray): - print(f"Image {i} shape: {img.shape}, dtype: {img.dtype}") - else: - print(f"Unexpected data type for image {i}") - print(f"Image {i} cache_key: {cache_key}") - - result = self.image_processor.process_image_sequence(image_data, cache_keys) - - end_time = time.time() - processing_time = end_time - start_time - - print(f"Processed image sequence for time range: {result['time_range']}") - print(f"Average time between frames: {result['sequence_period_seconds']:.2f} minutes") - print(f"Processing time: {processing_time:.2f} seconds") - - return result - - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing image sequence: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - return None - -class ImageProcessingNode: - def __init__(self, bootstrap_servers, input_topic, model_dir, group_id, redis_pool_db0, redis_pool_db1): - self.consumer = KafkaConsumer( - input_topic, - bootstrap_servers=bootstrap_servers, - group_id=group_id, - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - auto_offset_reset='earliest' - ) - self.producer = KafkaProducer( - bootstrap_servers=bootstrap_servers, - value_serializer=lambda x: json.dumps(x, cls=JSONEncoder).encode('utf-8') - ) - self.input_topic = input_topic - self.analysis_system = ImageSequenceAnalysisSystem(model_dir) - self.group_id = group_id - self.redis_pool_db0 = redis_pool_db0 - self.redis_pool_db1 = redis_pool_db1 - self.lock = threading.Lock() - self.image_buffer = [] - self.image_info_buffer = [] - self.cache_key_buffer = [] # 新增:用于存储cache_key - - - def process_and_produce(self, message_value): - cache_key = message_value.get('cache_key') - etag = message_value.get('etag') - size = message_value.get('size') - object_key = message_value.get('object') - - # print(f"Consumer {self.group_id} received image with key_hash: {key_hash}") - - try: - with redis.Redis(connection_pool=self.redis_pool_db0) as redis_client_db0: - img_str = redis_client_db0.get(cache_key) - if img_str is None: - print(f"Error: Image data not found in Redis db0 for cache_key: {cache_key}") - return - - img_data = base64.b64decode(img_str) - nparr = np.frombuffer(img_data, np.uint8) - img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - - self.image_buffer.append(img) - self.image_info_buffer.append({ - 'key_name': cache_key, # 使用 cache_key 作为 key_name - 'etag': etag, - 'size': size, - 'object_key': object_key - # 'key_hash': key_hash - }) - self.cache_key_buffer.append(cache_key) - - if len(self.image_buffer) == 3: - result = self.analysis_system.process_image_sequence(self.image_buffer, self.cache_key_buffer) - - if result: - result_data = { - 'results': result, - 'image_sequence': self.image_info_buffer - } - - with self.lock: - with redis.Redis(connection_pool=self.redis_pool_db1) as redis_client_db1: - # 使用第一张图片的 cache_key 作为 sequence_key - sequence_key = f"{self.image_info_buffer[0]['key_name']}" - redis_client_db1.set(sequence_key, json.dumps(result_data)) - print(f"Consumer {self.group_id} processed and saved results to Redis db1 with sequence_key {sequence_key}") - print(f"Processed image sequence: {[info['key_name'] for info in self.image_info_buffer]}") - - # 清空缓冲区 - self.image_buffer = [] - self.image_info_buffer = [] - self.cache_key_buffer = [] - - except Exception as e: - print(f"Error processing image: {str(e)}") - import traceback - traceback.print_exc() - - def run(self): - print(f"Consumer {self.group_id} starting to consume messages from {self.input_topic}") - while True: - messages = self.consumer.poll(timeout_ms=1000) - for tp, records in messages.items(): - for record in records: - message_value = record.value - self.process_and_produce(message_value) - - # 每分钟打印一次当前缓冲区状态 - if len(self.image_buffer) < 3: - print(f"Still waiting for more images. Current buffer size: {len(self.image_buffer)}") - time.sleep(60) # 等待60秒(1分钟) - -def start_consumer(kafka_bootstrap_servers, input_topic, model_dir, group_id, redis_host, redis_port, redis_password): - redis_pool_db0 = ConnectionPool(host=redis_host, port=redis_port, db=0, password=redis_password) - redis_pool_db1 = ConnectionPool(host=redis_host, port=redis_port, db=2, password=redis_password) - - node = ImageProcessingNode( - kafka_bootstrap_servers, - input_topic, - model_dir, - group_id, - redis_pool_db0, - redis_pool_db1 - ) - print(f"Image Processing Node {group_id} initialized.") - print(f"Listening on topic: {input_topic}") - node.run() - - -if __name__ == "__main__": - # Configuration - # kafka_bootstrap_servers = ['222.186.136.78:9092'] - # input_topic = 'cpm-input' - # model_dir = 'worker_sys/OpenBMB/MiniCPM-V-2_6' - # redis_host = '222.186.136.78' - # redis_port = 6379 - # redis_password = 'Obscura@2024' - # num_consumers = 1 - - kafka_bootstrap_servers = config['kafka']['bootstrap_servers'] - input_topic = config['kafka']['topics']['ten_seconds']['name'] - model_dir = config['model']['cpm-path'] - redis_host = config['redis']['host'] - redis_port = config['redis']['port'] - redis_password = config['redis']['password'] - num_consumers = config['kafka']['topics']['ten_seconds']['num_consumers'] - - # 创建多个消费者线程 - consumer_threads = [] - for i in range(num_consumers): - group_id = f'cpm_group_{i}' - thread = threading.Thread( - target=start_consumer, - args=(kafka_bootstrap_servers, input_topic, model_dir, group_id, redis_host, redis_port, redis_password) - ) - consumer_threads.append(thread) - thread.start() - - # 等待所有消费者线程完成 - for thread in consumer_threads: - thread.join() diff --git a/api_history/function/pose.py b/api_history/function/pose.py deleted file mode 100644 index a63e3bc..0000000 --- a/api_history/function/pose.py +++ /dev/null @@ -1,183 +0,0 @@ - -import json -import cv2 -import numpy as np -from kafka import KafkaConsumer -from ultralytics import YOLO -import threading -import redis -import base64 -import yaml -from PIL import Image - -# 加载配置文件 -with open('worker_sys/function/config.yaml', 'r') as file: - config = yaml.safe_load(file) - -class YOLOv8nPoseProcessor: - def __init__(self, model_path): - self.model = YOLO(model_path) - - def process_image(self, img): - results = self.model(img) - return results - - def format_results(self, results): - result_data = [] - for result in results: - boxes = result.boxes.xywh.tolist() - keypoints = result.keypoints.xy.tolist() if hasattr(result, 'keypoints') else None - classes = result.boxes.cls.tolist() - confs = result.boxes.conf.tolist() - - for i, (box, cls, conf) in enumerate(zip(boxes, classes, confs)): - result_data.append({ - 'box': box, - 'keypoints': keypoints[i] if keypoints else None, - 'class': int(cls), - 'class_name': self.model.names[int(cls)], - 'confidence': float(conf) - }) - - if not result_data: - result_data.append({ - 'box': None, - 'keypoints': None, - 'class': None, - 'class_name': None, - 'confidence': None, - 'message': 'No object detected' - }) - - return json.dumps(result_data) - -class ImageProcessingNode: - def __init__(self, bootstrap_servers, input_topic, model_path, group_id, redis_host, redis_port, redis_password): - self.consumer = KafkaConsumer( - input_topic, - bootstrap_servers=bootstrap_servers, - group_id=group_id, - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - auto_offset_reset='earliest' - ) - self.input_topic = input_topic - self.yolo_processor = YOLOv8nPoseProcessor(model_path) - self.group_id = group_id - self.redis_client_db0 = redis.Redis(host=redis_host, port=redis_port, db=0, password=redis_password) - self.redis_client_db1 = redis.Redis(host=redis_host, port=redis_port, db=1, password=redis_password) - - def process_and_produce(self, message): - - cache_key = message.get('cache_key') - object_key = message.get('object') - etag = message.get('etag') - size = message.get('size') - - print(f"Consumer {self.group_id} processing image with cache_key: {cache_key}") - - - # 从Redis db0获取图片数据 - img_str = self.redis_client_db0.get(cache_key) - if img_str is None: - print(f"Error: Image data not found in Redis db0 for cache_key: {cache_key}") - return - - # 将base64编码的图片数据转换为OpenCV格式 - try: - img_data = base64.b64decode(img_str) - nparr = np.frombuffer(img_data, np.uint8) - img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - - if img is None: - print(f"Error: Unable to decode image data for cache_key: {cache_key}") - return - - results = self.yolo_processor.process_image(img) - formatted_results = self.yolo_processor.format_results(results) - - # 创建包含所有必要信息的字典 - result_data = { - "pose_results": json.loads(formatted_results), - 'cache_key': cache_key, - "object": object_key, - "etag": etag, - "size": size - } - - # 将结果保存到Redis db1 - try: - serialized_data = json.dumps(result_data) - if not serialized_data: - print(f"Error: Serialized data is empty for cache_key: {cache_key}") - return - - self.redis_client_db1.set(cache_key, serialized_data) - print(f"Consumer {self.group_id} processed and saved results to Redis db1 with cache_key {cache_key}") - except TypeError as e: - print(f"Error serializing data: {e}") - print(f"Problematic data: {result_data}") - except Exception as e: - print(f"Unexpected error when saving to Redis: {e}") - except Exception as e: - print(f"Error processing image: {e}") - - - def run(self): - print(f"Consumer {self.group_id} starting to consume messages from {self.input_topic}") - for message in self.consumer: - message_value = message.value - cache_key = message_value.get('cache_key') - if cache_key: - threading.Thread(target=self.process_and_produce, args=(message_value,)).start() - else: - print(f"Consumer {self.group_id} error: Received message without cache_key") - -def start_consumer(kafka_bootstrap_servers, input_topic, model_path, group_id, redis_host, redis_port, redis_password): - node = ImageProcessingNode( - kafka_bootstrap_servers, - input_topic, - model_path, - group_id, - redis_host, - redis_port, - redis_password - ) - print(f"Image Processing Node {group_id} initialized.") - print(f"Listening on topic: {input_topic}") - node.run() - -if __name__ == "__main__": - # 创建多个消费者线程 - # Kafka 配置 - kafka_bootstrap_servers = config['kafka']['bootstrap_servers'] - input_topic = config['kafka']['topics']['all_frames']['name'] - num_consumers = config['kafka']['topics']['all_frames']['num_consumers'] - # 模型路径 - model_path = config['model']['pose-path'] - # Redis 配置 - redis_host = config['redis']['host'] - redis_port = config['redis']['port'] - redis_password = config['redis']['password'] - - - consumer_threads = [] - for i in range(num_consumers): - group_id = f'pose_group_{i}' - thread = threading.Thread( - target=start_consumer, - args=( - kafka_bootstrap_servers, - input_topic, - model_path, - group_id, - redis_host, - redis_port, - redis_password - ) - ) - consumer_threads.append(thread) - thread.start() - - # 等待所有消费者线程完成 - for thread in consumer_threads: - thread.join() diff --git a/api_history/function/producer-minio.py b/api_history/function/producer-minio.py deleted file mode 100644 index 60c0d40..0000000 --- a/api_history/function/producer-minio.py +++ /dev/null @@ -1,204 +0,0 @@ -import json -import yaml -from kafka import KafkaConsumer, KafkaProducer -import time -from datetime import datetime -import redis -from minio import Minio -import io -from PIL import Image -import base64 -import traceback - - -# 加载配置文件 -with open('worker_sys/function/config.yaml', 'r') as file: - config = yaml.safe_load(file) - -# Kafka 配置 -kafka_config = { - 'bootstrap_servers': config['kafka']['bootstrap_servers'], - 'value_serializer': lambda v: json.dumps(v).encode('utf-8') if config['kafka']['value_serializer'] == 'json' else None -} - -# 创建Kafka生产者 -producer = KafkaProducer(**kafka_config) - -# 创建Kafka消费者(用于输入主题) -consumer = KafkaConsumer( - config['kafka']['input_topic']['name'], - group_id='image-processor', - bootstrap_servers=config['kafka']['bootstrap_servers'] -) - -# Redis 配置 -redis_config = config['redis'] - -# 创建 Redis 客户端 -redis_client = redis.Redis(**redis_config) - -# MinIO 配置 -minio_config = config['minio'] - -# 创建 MinIO 客户端 -minio_client = Minio( - minio_config['endpoint'], - access_key=minio_config['access_key'], - secret_key=minio_config['secret_key'], - secure=minio_config['secure'] -) - -# Kafka topics -topic_all_frames = config['kafka']['topics']['all_frames']['name'] -topic_ten_seconds = config['kafka']['topics']['ten_seconds']['name'] -topic_input = config['kafka']['input_topic']['name'] - -# 消费者数量 -NUM_CONSUMERS_ALL_FRAMES = config['kafka']['topics']['all_frames']['num_consumers'] -NUM_CONSUMERS_TEN_SECONDS = config['kafka']['topics']['ten_seconds']['num_consumers'] -NUM_CONSUMERS_INPUT = config['kafka']['input_topic']['num_consumers'] - -def parse_key_name(key_name): - parts = key_name.split('/') - bucket = parts[0] - image_name = '/'.join(parts[1:]) - image_parts = image_name.rsplit('.', 1)[0].split('_') - camera_name = image_parts[0] - timestamp = '_'.join(image_parts[1:3]) - return bucket, camera_name, timestamp - -def should_send_to_ten_seconds_topic(timestamp): - try: - dt = datetime.strptime(timestamp, '%Y%m%d_%H%M%S') - return dt.second % 10 == 0 - except ValueError as e: - print(f"Error parsing timestamp '{timestamp}': {str(e)}") - return False - -def get_image_from_minio_and_cache(key_name): - bucket, object_name = key_name.split('/', 1) - cache_key = f"{key_name}" - - try: - response = minio_client.get_object(bucket, object_name) - image_data = response.read() - print(f"Successfully retrieved image from MinIO: {bucket}/{object_name}") - - image = Image.open(io.BytesIO(image_data)) - buffered = io.BytesIO() - image.save(buffered, format="JPEG") - img_str = base64.b64encode(buffered.getvalue()).decode() - - redis_client.setex(cache_key, 86400, img_str) - print(f"Successfully cached image in Redis: {cache_key}") - - return cache_key - except Exception as e: - print(f"Error in get_image_from_minio_and_cache: {str(e)}") - print("Traceback:") - traceback.print_exc() - return None - -def process_message(message): - - value_data = json.loads(message.value.decode('utf-8')) - key_name = value_data['Key'] - print(f"Processing key: {key_name}") - - # 从 key_name 中提取 bucket - parts = key_name.split('/', 1) - if len(parts) != 2: - print(f"Error: Invalid key format. Expected 'bucket/object', got '{key_name}'") - return - - bucket, object_name = parts - - # 解析 camera_name 和 timestamp - camera_name, timestamp = parse_object_name(object_name) - - cache_key = get_image_from_minio_and_cache(key_name) - - if cache_key is None: - print(f"Failed to process image: {key_name}") - return - # 提取 etag 和 size 信息 - object_info = value_data['Records'][0]['s3']['object'] - etag = object_info.get('eTag', '') - size = object_info.get('size', 0) - - - message_data = { - 'bucket': bucket, - 'camera_name': camera_name, - 'timestamp': timestamp, - 'object': object_name, - 'cache_key': cache_key, - 'etag': etag, - 'size': size - } - - # 发送到 pose-input 主题 - producer.send(topic_all_frames, value=message_data) - print(f"Sent message to {topic_all_frames}: {key_name}") - - # 发送到 cpm-input 主题 - producer.send(topic_ten_seconds, value=message_data) - print(f"Sent message to {topic_ten_seconds}: {key_name}") - - # #只有在满足特定条件时才发送到 cpm-input 主题 - # if should_send_to_ten_seconds_topic(timestamp): - # producer.send(TOPIC_TEN_SECONDS, value=message_data) - # print(f"Sent message to {TOPIC_TEN_SECONDS}: {key_name}") - - producer.flush() - print(f"Successfully processed and sent messages for: {key_name}") - - -def parse_object_name(object_name): - # 假设对象名格式为 "cameraX_YYYYMMDD_HHMMSS.jpg" - parts = object_name.split('_') - if len(parts) != 3: - raise ValueError(f"Invalid object name format: {object_name}") - - camera_name = parts[0] - timestamp = f"{parts[1]}_{parts[2].split('.')[0]}" - return camera_name, timestamp - -def get_image_from_minio_and_cache(key_name): - print(f"Received key_name: {key_name}") - - try: - bucket, object_name = key_name.split('/', 1) - except ValueError as e: - print(f"Error splitting key_name '{key_name}': {str(e)}") - return None - - cache_key = f"{key_name}" - - try: - response = minio_client.get_object(bucket, object_name) - image_data = response.read() - print(f"Successfully retrieved image from MinIO: {bucket}/{object_name}") - - image = Image.open(io.BytesIO(image_data)) - buffered = io.BytesIO() - image.save(buffered, format="JPEG") - img_str = base64.b64encode(buffered.getvalue()).decode() - - redis_client.setex(cache_key, 86400, img_str) - print(f"Successfully cached image in Redis: {cache_key}") - - return cache_key - except Exception as e: - print(f"Error in get_image_from_minio_and_cache: {str(e)}") - print("Traceback:") - traceback.print_exc() - return None - -def main(): - print("Starting image processing service...") - for message in consumer: - process_message(message) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/api_history/function/sync.py b/api_history/function/sync.py deleted file mode 100644 index 3148628..0000000 --- a/api_history/function/sync.py +++ /dev/null @@ -1,91 +0,0 @@ -import redis -import pymongo -import time -import json -import yaml - -def sync_data(redis_client, mongo_db, redis_db, mongo_collection_name): - mongo_collection = mongo_db[mongo_collection_name] - all_keys = redis_client.keys('*') - synced_count = 0 - - for key in all_keys: - try: - poem_data = redis_client.get(key) - - if poem_data: - poem_dict = json.loads(poem_data) - - # 检查MongoDB中是否已存在相同的文档 - existing_doc = mongo_collection.find_one({'_id': key.decode('utf-8')}) - - if not existing_doc or existing_doc != poem_dict: - mongo_collection.update_one( - {'_id': key.decode('utf-8')}, - {'$set': poem_dict}, - upsert=True - ) - print(f"Synced data with key: {key} from Redis DB {redis_db} to MongoDB collection '{mongo_collection_name}'") - synced_count += 1 - except json.JSONDecodeError: - print(f"Error decoding JSON for key: {key} in Redis DB {redis_db}") - except Exception as e: - print(f"Error syncing data for key {key} in Redis DB {redis_db}: {str(e)}") - - return synced_count - -def main(config): - # Redis配置 - redis_host = config['redis']['host'] - redis_port = config['redis']['port'] - redis_password = config['redis']['password'] - - # MongoDB配置 - mongo_uri = config['mongodb']['uri'] - mongo_db_name = config['mongodb']['db_name'] - - # 连接到MongoDB - mongo_client = pymongo.MongoClient(mongo_uri) - mongo_db = mongo_client[mongo_db_name] - - # 固定的Redis数据库和MongoDB集合映射 - db_collection_map = { - 0: 'pose-result-db0', - 1: 'pose-result-db1', - 2: 'cpm-result-db2' - } - - print("Selected databases and collections for syncing:") - for db, collection in db_collection_map.items(): - print(f" Redis DB {db} -> MongoDB collection '{collection}'") - - while True: - print("Starting sync...") - total_synced = 0 - for db, collection in db_collection_map.items(): - print(f"Syncing Redis DB {db} to MongoDB collection '{collection}'...") - try: - redis_client = redis.Redis(host=redis_host, port=redis_port, db=db, password=redis_password) - synced_count = sync_data(redis_client, mongo_db, db, collection) - total_synced += synced_count - except redis.exceptions.AuthenticationError: - print(f"Error: Authentication failed for Redis DB {db}. Skipping...") - except redis.exceptions.ConnectionError: - print(f"Error: Unable to connect to Redis DB {db}. Skipping...") - except Exception as e: - print(f"Error occurred while syncing Redis DB {db}: {str(e)}. Skipping...") - - if total_synced > 0: - print(f"Sync completed. {total_synced} documents synced. Waiting for next update...") - else: - print("No new data to sync. Waiting for next update...") - - time.sleep(300) # 等待5分钟后再次同步 - -if __name__ == "__main__": - # 加载配置文件 - with open('worker_sys/function/config.yaml', 'r') as file: - config = yaml.safe_load(file) - - # 运行主程序 - main(config) \ No newline at end of file diff --git a/api_history/local/local_cpm.py b/api_history/local/local_cpm.py deleted file mode 100644 index 7fe3fad..0000000 --- a/api_history/local/local_cpm.py +++ /dev/null @@ -1,299 +0,0 @@ -import torch -from PIL import Image -from transformers import AutoModel, AutoTokenizer -import json -import re -from pymongo import MongoClient -import time -from bson import ObjectId -import os -import glob -from datetime import datetime, timedelta - -# 数据库连接模块 -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, results_collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.results_collection = self.db[results_collection_name] - - def save_result(self, result): - # 如果 result 中没有 filename,使用时间戳作为替代 - # filename = result.get('filename', f"unknown_{result['timestamp']}") - filename = result.get('filename') - # 检查是否已存在相同 filename 的结果 - existing_result = self.results_collection.find_one({'filename': filename}) - if existing_result: - print(f"Video with filename {filename} has already been processed. Skipping.") - return - - # 确保 result 中有 filename - result['filename'] = filename - - # 将 ObjectId 转换为字符串 - if 'video_id' in result and isinstance(result['video_id'], ObjectId): - result['video_id'] = str(result['video_id']) - - self.results_collection.insert_one(result) - - def is_sequence_processed(self, filename): - return self.results_collection.find_one({'filename': filename}) is not None - - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return super().default(o) - -# 视频处理模块 -class ImageSequenceProcessor: - def __init__(self, model_dir): - self.model = AutoModel.from_pretrained(model_dir, trust_remote_code=True, - attn_implementation='sdpa', torch_dtype=torch.bfloat16).eval().cuda() - self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) - self.max_size = 512 # 设置最大尺寸 - - def compress_image(self, image): - # 保持纵横比的情况下调整图片大小 - image.thumbnail((self.max_size, self.max_size)) - - # 如果图像已经是JPEG格式,直接返回调整大小后的图像 - if image.format == 'JPEG': - return image - - # 对于非JPEG格式,进行压缩 - buffer = io.BytesIO() - if image.mode in ('RGBA', 'LA') or (image.mode == 'P' and 'transparency' in image.info): - # 保持透明度 - image.save(buffer, format="PNG", optimize=True) - else: - # 转换为JPEG并压缩 - image.convert('RGB').save(buffer, format="JPEG", quality=85, optimize=True) - - buffer.seek(0) - return Image.open(buffer) - - def process_image_sequence(self, image_paths): - frames = [self.compress_image(Image.open(img_path)) for img_path in image_paths] - question = "Analyze these 10 images as if they were frames from a video. Describe the scene in detail in Chinese, including the setting, number of people, their actions, and any changes or movements observed across the frames." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - } - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - # 环境提取 - environments = ["办公室", "室内", "室外", "会议室", "办公"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - # 改进的人数提取 - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - # 动作和互动提取 - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - # 物体和家具提取 - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 主处理类 -class ImageSequenceAnalysisSystem: - def __init__(self, mongo_uri, db_name, model_dir, results_collection_name): - self.db_handler = DatabaseHandler(mongo_uri, db_name, results_collection_name) - self.image_processor = ImageSequenceProcessor(model_dir) - self.last_processed_time = datetime.now() - timedelta(hours=1) - - def get_all_images(self, image_folders): - image_files = [] - for folder in image_folders: - image_files.extend(glob.glob(os.path.join(folder, '*.jpg'))) - image_files.sort() - return image_files - - - def process_image_sequence(self, image_paths): - print(f"Attempting to process sequence: {[os.path.basename(img) for img in image_paths]}") - start_time = time.time() - try: - # 使用第一张图片的文件名作为序列的标识符 - filename = os.path.basename(image_paths[0]) - - if self.db_handler.is_sequence_processed(filename): - print(f"Skipping already processed image sequence: {filename}") - return False - - print("Processing new image sequence...") - result = self.image_processor.process_image_sequence(image_paths) - - # timestamp = datetime.now() - # result['timestamp'] = timestamp.strftime("%Y%m%d_%H%M%S") - result['image_paths'] = image_paths - result['filename'] = filename - - # 计算图片序列的周期 - image_times = [self.get_file_time(img) for img in image_paths] - if len(image_times) >= 2: - time_diff = (image_times[-1] - image_times[0]).total_seconds() - period_minutes = time_diff / (len(image_times) - 1) / 60 - result['sequence_period_minutes'] = round(period_minutes, 2) - - # 添加时间段信息 - result['time_range'] = { - 'start': image_times[0].strftime("%Y-%m-%d %H:%M"), - 'end': image_times[-1].strftime("%Y-%m-%d %H:%M") - } - - save_result = self.db_handler.save_result(result) - print(f"Result saved to: {self.db_handler.results_collection.name}") - print(f"Result filename: {filename}") - return save_result - - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing image sequence: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - return False - # @staticmethod - # def extract_time_from_filename(filename): - # # 假设文件名格式为 "YYYYMMDDHHMMSS.jpg" - # time_str = filename.split('.')[0] - # return datetime.strptime(time_str, "%Y%m%d%H%M") - - @staticmethod - def get_file_time(file_path): - # 获取文件的修改时间 - mod_time = os.path.getmtime(file_path) - return datetime.fromtimestamp(mod_time) - - def process_all_unprocessed_images(self, image_folders): - print(f"Searching for unprocessed images in: {image_folders}") - all_images = self.get_all_images(image_folders) - print(f"Found {len(all_images)} images in total") - selected_images = all_images[::10] - # Group images into sequences of 3 - image_sequences = [selected_images[i:i+10] for i in range(0, len(selected_images), 10)] - # image_sequences = [all_images[i:i+10] for i in range(0, len(all_images), 10)] - - processed_sequences = 0 - for sequence in image_sequences: - if len(sequence) == 10: - self.process_image_sequence(sequence) - processed_sequences += 1 - else: - print(f"Warning: Incomplete sequence. Found {len(sequence)} images.") - - if processed_sequences == 0: - print("All current photos have been processed. Waiting for new photos...") - else: - print(f"Processed {processed_sequences} sequences.") - - def run(self, root_folders): - print(f"Starting the system with root folder: {', '.join(root_folders)}") - - while True: - current_time = datetime.now() - time_since_last_process = (current_time - self.last_processed_time).total_seconds() - - if time_since_last_process >= 3600: # 1小时 = 3600秒 - self.process_all_unprocessed_images(root_folders) - self.last_processed_time = current_time - - # 计算下次检查的等待时间 - wait_time = max(0, 3600 - (datetime.now() - self.last_processed_time).total_seconds()) - print(f"Waiting for new photos... Next check in {wait_time:.0f} seconds.") - - time.sleep(60) # 每分钟检查一次是否需要处理 -# 使用示例 -if __name__ == "__main__": - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - results_collection_name = "cpm" - - model_dir = "worker_sys/OpenBMB/MiniCPM-V-2_6" - - root_folders = [ - "/www/wwwroot/zj.obscura.ac.cn/ipcam/Office/Cam2/CapturePics" , - "/www/wwwroot/zj.obscura.ac.cn/ipcam/Office/Cam1/CapturePics" - ] # 修改为 cam1 文件夹的路径 - - system = ImageSequenceAnalysisSystem(mongo_uri, db_name, model_dir, results_collection_name) - system.run(root_folders) \ No newline at end of file diff --git a/api_history/local/local_pose.py b/api_history/local/local_pose.py deleted file mode 100644 index 6fefc75..0000000 --- a/api_history/local/local_pose.py +++ /dev/null @@ -1,194 +0,0 @@ -import torch -from PIL import Image -import json -from pymongo import MongoClient -import os -import glob -from datetime import datetime, timedelta -from ultralytics import YOLO -from bson import ObjectId -import time - -# 数据库连接模块 -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, results_collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.results_collection = self.db[results_collection_name] - - def save_result(self, result): - filename = result.get('filename', f"unknown_{result['timestamp']}") - - existing_result = self.results_collection.find_one({'filename': filename}) - if existing_result: - print(f"Image with filename {filename} has already been processed. Skipping.") - return False - - result['filename'] = filename - - if 'image_id' in result and isinstance(result['image_id'], ObjectId): - result['image_id'] = str(result['image_id']) - - self.results_collection.insert_one(result) - return True - - def is_image_processed(self, filename): - return self.results_collection.find_one({'filename': filename}) is not None - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return super().default(o) - -# YOLOv8nPoseProcessor 类 -class YOLOv8nPoseProcessor: - def __init__(self, model_path): - self.model = YOLO(model_path) - - def process_image(self, img): - results = self.model(img) - return results - - def format_results(self, results): - result_data = [] - for result in results: - boxes = result.boxes.xywh.tolist() - keypoints = result.keypoints.xy.tolist() if hasattr(result, 'keypoints') else None - classes = result.boxes.cls.tolist() - confs = result.boxes.conf.tolist() - - for i, (box, cls, conf) in enumerate(zip(boxes, classes, confs)): - result_data.append({ - 'box': box, - 'keypoints': keypoints[i] if keypoints else None, - 'class': int(cls), - 'class_name': self.model.names[int(cls)], - 'confidence': float(conf) - }) - - if not result_data: - result_data.append({ - 'box': None, - 'keypoints': None, - 'class': None, - 'class_name': None, - 'confidence': None, - 'message': 'No object detected' - }) - - return json.dumps(result_data) - -# 主处理类 -class ImageAnalysisSystem: - def __init__(self, mongo_uri, db_name, model_path, results_collection_name): - self.db_handler = DatabaseHandler(mongo_uri, db_name, results_collection_name) - self.image_processor = YOLOv8nPoseProcessor(model_path) - self.last_processed_time = datetime.now() - timedelta(hours=1) - - def get_all_images(self, image_folders): - image_files = [] - for folder in image_folders: - image_files.extend(glob.glob(os.path.join(folder, '*.jpg'))) - image_files.sort() - return image_files - @staticmethod - def get_file_time(file_path): - # 获取文件的修改时间 - mod_time = os.path.getmtime(file_path) - return datetime.fromtimestamp(mod_time) - def process_image(self, image_path): - print(f"Attempting to process image: {os.path.basename(image_path)}") - try: - # json_folder = os.path.join("/www/wwwroot/zj.obscura.ac.cn/ipcam/Office/Cam2", 'json') - # json_filename = f"{os.path.basename(image_path).split('.')[0]}.json" - # json_path = os.path.join(json_folder, json_filename) - - # if os.path.exists(json_path): - # print(f"Skipping already processed image: {json_path}") - # return - - filename = os.path.basename(image_path) - - if self.db_handler.is_image_processed(filename): - print(f"Skipping already processed image: {filename}") - return False - - print("Processing new image...") - - image = Image.open(image_path) - results = self.image_processor.process_image(image) - formatted_results = self.image_processor.format_results(results) - - # timestamp = datetime.now() - file_timestamp = self.get_file_time(image_path) - result = { - 'timestamp': file_timestamp.strftime("%Y%m%d_%H%M%S"), - 'image_path': image_path, - 'filename': os.path.basename(image_path), - 'results': json.loads(formatted_results) - } - - # os.makedirs(json_folder, exist_ok=True) - # with open(json_path, 'w', encoding='utf-8') as f: - # json.dump(result, f, ensure_ascii=False, indent=4, cls=JSONEncoder) - - # self.db_handler.save_result(result) - # print(f"Processed image at: {timestamp}") - # print(f"JSON saved to: {json_path}") - # print(f"result saved to: {results_collection_name}") - if self.db_handler.save_result(result): - print(f"Result saved to: {self.db_handler.results_collection.name}") - return True - else: - print(f"Image {filename} was already in the database. Skipping.") - return False - - except Exception as e: - print(f"Error processing image: {str(e)}") - import traceback - traceback.print_exc() - - def process_all_unprocessed_images(self, image_folders): - print(f"Searching for unprocessed images in: {image_folders}") - all_images = self.get_all_images(image_folders) - print(f"Found {len(all_images)} images in total") - - processed_count = 0 - for image_path in all_images: - if self.process_image(image_path): - processed_count += 1 - - return processed_count - - def run(self, root_folder): - print(f"Starting the system with root folder: {', '.join(root_folder)}") - # image_folder = os.path.join(root_folder) - - while True: - print("Checking for unprocessed images...") - processed_count = self.process_all_unprocessed_images(root_folder) - - if processed_count > 0: - print(f"Finished processing {processed_count} images.") - else: - print("No new images to process. Waiting for new images...") - - # 等待一段时间后再次检查新图片 - time.sleep(60) # 每分钟检查一次是否有新图片 - -# 使用示例 -if __name__ == "__main__": - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - results_collection_name = "pose" - - model_path = "worker_sys/function/yolov8x-pose.pt" # 请确保这个路径指向你的YOLO-Pose模型文件 - - root_folder = [ - "/www/wwwroot/zj.obscura.ac.cn/ipcam/Office/Cam2/CapturePics" , - "/www/wwwroot/zj.obscura.ac.cn/ipcam/Office/Cam1/CapturePics" - ] # 修改为 cam1 文件夹的路径 - - system = ImageAnalysisSystem(mongo_uri, db_name, model_path, results_collection_name) - system.run(root_folder) \ No newline at end of file diff --git a/api_history/main.py b/api_history/main.py deleted file mode 100644 index 078fd33..0000000 --- a/api_history/main.py +++ /dev/null @@ -1,426 +0,0 @@ -# main.py -from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.security import APIKeyHeader -from kafka import KafkaProducer -from redis import Redis -import os -import json -import uuid -from datetime import datetime, timedelta, timezone -import string -from decord import VideoReader -from PIL import Image -from fastapi.responses import FileResponse -import logging -from config import * - -app = FastAPI() -v1_app = FastAPI() -app.mount("/v1", v1_app) - - -# CORS设置 -# ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -KAFKA_BROKER = KAFKA_BROKER -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD -REDIS_DB = MAIN_REDIS_DB -REDIS_API_DB = REDIS_API_DB -REDIS_API_USAGE_DB = REDIS_API_USAGE_DB -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 定义支持的任务类型 -KAFKA_TOPICS = { - 'pose': 'pose', - 'mediapipe': 'mediapipe', - 'qwenvl': 'qwenvl', - 'yolo': 'yolo', - 'fall': 'fall', - 'face': 'face', - 'cpm': 'cpm', - 'compare': 'compare' -} - -TASK_TYPES = list(KAFKA_TOPICS.keys()) - - -# 初始化 Kafka Producer -producer = KafkaProducer( - bootstrap_servers=[KAFKA_BROKER], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) - -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) -redis_pose_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['pose']['redis_db']) -redis_cpm_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['cpm']['redis_db']) -redis_yolo_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['yolo']['redis_db']) -redis_face_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['face']['redis_db']) -redis_fall_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['fall']['redis_db']) -redis_mediapipe_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['mediapipe']['redis_db']) -redis_qwenvl_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['qwenvl']['redis_db']) -redis_compare_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['compare']['redis_db']) -@v1_app.get('/favicon.ico', include_in_schema=False) -async def favicon(): - file_name = "favicon.ico" - file_path = os.path.join(app.root_path, "static", file_name) - if os.path.isfile(file_path): - return FileResponse(file_path) - else: - return {"message": "Favicon not found"}, 404 - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -async def verify_api_key(api_key: str = Depends(get_api_key)): - logging.info(f"验证API密钥: {api_key}") - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - logging.warning(f"API密钥不存在: {api_key}") - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - logging.warning(f"API密钥已停用: {api_key}") - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - logging.warning(f"API密钥已过期: {api_key}") - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - logging.info(f"API密钥验证成功: {api_key}") - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 0.1) - - if file_type == "image": - img = Image.open(file_path) - width, height = img.size - pixel_count = width * height - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 100000000) * 0.1) - - base_tokens += image_tokens - - elif file_type == "video": - vr = VideoReader(file_path) - fps = vr.get_avg_fps() - frame_count = len(vr) - width, height = vr[0].shape[1], vr[0].shape[0] - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 100000000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - - -async def upload_file(file: UploadFile, task_type: str, api_key_info: dict): - if task_type not in KAFKA_TOPICS: - raise HTTPException(status_code=400, detail="不支持的任务类型") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(file_path, file_type) - - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - await update_token_usage(api_key, tokens_required, task_type) - - # 创建任务记录 - task_id = str(uuid.uuid4()) - task_data = { - "task_id": task_id, - "filename": new_filename, - "file_type": file_type, - "task_type": task_type, - "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), - } - - # 存储任务信息到Redis - redis_client.set(f"task:{task_id}", json.dumps(task_data)) - logging.info(f"任务信息已存储到Redis: {task_id}") - - # 发送任务到对应的Kafka主题 - kafka_topic = KAFKA_TOPICS[task_type] - producer.send(kafka_topic, task_data) - logging.info(f"任务已发送到Kafka主题: {kafka_topic}") - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{task_type}_tokens_used", 0)) - - response_data = { - "message": "文件已上传并排队等待处理", - "task_id": task_id, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{task_type}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - } - logging.info(f"上传文件完成: {task_id}") - return JSONResponse(content=response_data) - -# 为每个任务类型创建单独的端点 -@v1_app.post("/pose") -async def upload_pose(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - logging.info(f"收到 /pose端点的请求") - return await upload_file(file, task_type="pose", api_key_info=api_key_info) - -@v1_app.post("/cpm") -async def upload_cpm(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="cpm", api_key_info=api_key_info) - -@v1_app.post("/qwenvl") -async def upload_qwenvl(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="qwenvl", api_key_info=api_key_info) - -@v1_app.post("/yolo") -async def upload_yolo(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="yolo", api_key_info=api_key_info) - -@v1_app.post("/fall") -async def upload_fall(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="fall", api_key_info=api_key_info) - -@v1_app.post("/face") -async def upload_face(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - logging.info(f"收到 /face 端点的请求") - return await upload_file(file, task_type="face", api_key_info=api_key_info) - -@v1_app.post("/mediapipe") -async def upload_mediapipe(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="mediapipe", api_key_info=api_key_info) - -@v1_app.post("/compare") -async def upload_compare(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="compare", api_key_info=api_key_info) - - -@v1_app.get("/result/{task_id}") -async def get_result(task_id: str, api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - # 从 REDIS_DB (15) 获取任务状态 - task_info = redis_client.hgetall(f"task:{task_id}") - if not task_info: - raise HTTPException(status_code=404, detail="Task not found") - - task_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in task_info.items()} - - if task_info['status'] != 'completed': - return {"status": task_info['status'], "message": "Task is not completed yet"} - - result_type = task_info['result_type'] - result_key = task_info['result_key'] - - # 根据任务类型选择相应的 Redis 客户端 - redis_client_map = { - 'pose': redis_pose_client, - 'cpm': redis_cpm_client, - 'yolo': redis_yolo_client, - 'face': redis_face_client, - 'fall': redis_fall_client, - 'mediapipe': redis_mediapipe_client, - 'qwenvl': redis_qwenvl_client, - 'compare': redis_compare_client - } - - result_redis = redis_client_map.get(result_type) - if not result_redis: - raise HTTPException(status_code=400, detail="Unsupported result type") - - result = result_redis.hgetall(result_key) - if not result: - raise HTTPException(status_code=404, detail=f"{result_type.upper()} result not found") - - result = {k.decode('utf-8'): v.decode('utf-8') for k, v in result.items()} - - # 将 result 字段解析为 JSON(如果存在) - if 'result' in result: - result['result'] = json.loads(result['result']) - - return { - "status": "completed", - "result_type": result_type, - "result": result - } - -@v1_app.get("/annotated/{task_id}") -async def get_annotated_image(task_id: str, api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - # 从 REDIS_DB (15) 获取任务信息 - task_info = redis_client.hgetall(f"task:{task_id}") - if not task_info: - raise HTTPException(status_code=404, detail="Task not found") - - task_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in task_info.items()} - - if task_info['status'] != 'completed': - raise HTTPException(status_code=400, detail="Task is not completed yet") - - result_type = task_info.get('result_type') - result_key = task_info.get('result_key') - - if not result_key: - raise HTTPException(status_code=404, detail="Result key not found") - - if result_type in ['cpm', 'qwenvl']: - raise HTTPException(status_code=400, detail="Annotated image not available for this task type") - - # 根据任务类型选择相应的 Redis 客户端 - redis_client_map = { - 'pose': redis_pose_client, - 'yolo': redis_yolo_client, - 'face': redis_face_client, - 'fall': redis_fall_client, - 'mediapipe': redis_mediapipe_client, - 'compare': redis_compare_client - } - - result_redis = redis_client_map.get(result_type) - if not result_redis: - raise HTTPException(status_code=400, detail="Unsupported result type") - - result = result_redis.hgetall(result_key) - if not result: - raise HTTPException(status_code=404, detail=f"{result_type.upper()} result not found") - - result = {k.decode('utf-8'): v.decode('utf-8') for k, v in result.items()} - - result_file = result.get('result_file') - if not result_file: - raise HTTPException(status_code=404, detail="Result file not found") - - file_path = os.path.join(RESULT_DIR, result_file) - if not os.path.exists(file_path): - raise HTTPException(status_code=404, detail="Result image file not found") - - return FileResponse(file_path, media_type="image/png") - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8005) \ No newline at end of file diff --git a/api_history/mini_douyin_time.py b/api_history/mini_douyin_time.py deleted file mode 100644 index 54af528..0000000 --- a/api_history/mini_douyin_time.py +++ /dev/null @@ -1,255 +0,0 @@ -import torch -from PIL import Image -from transformers import AutoModel, AutoTokenizer -from decord import VideoReader, cpu -import json -import re -from pymongo import MongoClient -import io -from minio import Minio -import time -from bson import ObjectId -import concurrent.futures -import os - - -# Minio连接模块 -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=True - ) - - def get_video_data(self, bucket, object_name): - response = self.client.get_object(bucket, object_name) - data = response.read() - # print(f"Read {len(data)} bytes from Minio for {object_name}") - return data - -# 数据库连接模块 -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, results_collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.minio_files_collection = self.db['minio_files'] - self.results_collection = self.db[results_collection_name] - - def get_unprocessed_videos(self): - processed_etags = set(self.results_collection.distinct('etag')) - return self.minio_files_collection.find({ - 'bucket_name': 'raw', - 'object_name': {'$regex': r'^douyin/.*/.+\.(mp4|avi|mov|flv)$'}, - 'etag': {'$nin': list(processed_etags)} - }) - def save_result(self, result): - # 检查是否已存在相同 etag 的结果 - existing_result = self.results_collection.find_one({'etag': result['etag']}) - if existing_result: - print(f"Video with etag {result['etag']} has already been processed. Skipping.") - return - - # 将 ObjectId 转换为字符串 - if 'video_id' in result and isinstance(result['video_id'], ObjectId): - result['video_id'] = str(result['video_id']) - - self.results_collection.insert_one(result) - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return super().default(o) - - -# 视频处理模块 -class VideoProcessor: - def __init__(self, model_dir): - self.model = AutoModel.from_pretrained(model_dir, trust_remote_code=True, - attn_implementation='sdpa', torch_dtype=torch.bfloat16).eval().cuda() - self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) - self.MAX_NUM_FRAMES = 64 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - idxs = [int(i * gap + gap / 2) for i in range(n)] - return [l[i] for i in idxs] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = [i for i in range(0, len(vr), sample_fps)] - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_video(self, video_data, object_name): - if not video_data: - raise ValueError(f"Empty video data for {object_name}") - print(f"Processing video: {object_name}, data size: {len(video_data)} bytes") - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - } - - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - # 环境提取 - environments = ["办公室", "室内", "室外", "会议室", "办公"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - # 改进的人数提取 - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - # 动作和互动提取 - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - # 物体和家具提取 - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 主处理类 -class VideoAnalysisSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, results_collection_name) - self.video_processor = VideoProcessor(model_dir) - - def process_video(self, video_doc): - start_time = time.time() - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - result = self.video_processor.process_video(video_data, video_doc['object_name']) - - result['etag'] = video_doc['etag'] - result['bucket_name'] = video_doc['bucket_name'] - result['object_name'] = video_doc['object_name'] - - self.db_handler.save_result(result) - - end_time = time.time() - processing_time = end_time - start_time - - print(f"Processed video: {video_doc['object_name']}") - print(f"Processing time: {processing_time:.2f} seconds") - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - - def run(self): - while True: - unprocessed_videos = list(self.db_handler.get_unprocessed_videos()) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 60 seconds before checking again...") - time.sleep(60) - continue - - for video_doc in unprocessed_videos: - self.process_video(video_doc) - - print("Finished processing current batch of videos. Waiting for new videos...") - time.sleep(30) -# 使用示例 -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - results_collection_name = "douyin_results" - - model_dir = "MiniCPM-V-2_6" - - system = VideoAnalysisSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name) - system.run() \ No newline at end of file diff --git a/api_history/mini_up.py b/api_history/mini_up.py deleted file mode 100644 index 30de80c..0000000 --- a/api_history/mini_up.py +++ /dev/null @@ -1,248 +0,0 @@ -import torch -from PIL import Image -from transformers import AutoModel, AutoTokenizer -from decord import VideoReader, cpu -import json -import re -from pymongo import MongoClient -import io -from minio import Minio -import time -from bson import ObjectId - - -# Minio连接模块 -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=True - ) - - def get_video_data(self, bucket, object_name): - response = self.client.get_object(bucket, object_name) - data = response.read() - print(f"Read {len(data)} bytes from Minio for {object_name}") - return data - -# 数据库连接模块 -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, results_collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.minio_files_collection = self.db['minio_files'] - self.results_collection = self.db[results_collection_name] - - def get_unprocessed_videos(self): - # 查找 bucket_name 为 'raw-video' 且在结果集合中没有对应 etag 的视频 - processed_etags = set(self.results_collection.distinct('etag')) - return self.minio_files_collection.find({ - 'bucket_name': 'raw', - 'object_name': {'$regex': 'videoupload/'}, - 'etag': {'$nin': list(processed_etags)} - }) - - def save_result(self, result): - # 检查是否已存在相同 etag 的结果 - existing_result = self.results_collection.find_one({'etag': result['etag']}) - if existing_result: - print(f"Video with etag {result['etag']} has already been processed. Skipping.") - return - - # 将 ObjectId 转换为字符串 - if 'video_id' in result and isinstance(result['video_id'], ObjectId): - result['video_id'] = str(result['video_id']) - - self.results_collection.insert_one(result) - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return super().default(o) - - -# 视频处理模块 -class VideoProcessor: - def __init__(self, model_dir): - self.model = AutoModel.from_pretrained(model_dir, trust_remote_code=True, - attn_implementation='sdpa', torch_dtype=torch.bfloat16).eval().cuda() - self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) - self.MAX_NUM_FRAMES = 64 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - idxs = [int(i * gap + gap / 2) for i in range(n)] - return [l[i] for i in idxs] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = [i for i in range(0, len(vr), sample_fps)] - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_video(self, video_data, object_name): - if not video_data: - raise ValueError(f"Empty video data for {object_name}") - print(f"Processing video: {object_name}, data size: {len(video_data)} bytes") - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - } - - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - # 环境提取 - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - # 改进的人数提取 - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - # 动作和互动提取 - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - # 物体和家具提取 - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 主处理类 -class VideoAnalysisSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, results_collection_name) - self.video_processor = VideoProcessor(model_dir) - - def run(self): - while True: - unprocessed_videos = list(self.db_handler.get_unprocessed_videos()) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 60 seconds before checking again...") - time.sleep(1) - continue - - for video_doc in unprocessed_videos: - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - result = self.video_processor.process_video(video_data, video_doc['object_name']) - - # 添加额外信息到结果中 - result['etag'] = video_doc['etag'] - # result['video_id'] = str(video_doc['_id']) # 将 ObjectId 转换为字符串 - result['bucket_name'] = video_doc['bucket_name'] - result['object_name'] = video_doc['object_name'] - - # 保存结果到 MongoDB - self.db_handler.save_result(result) - - print(f"Processed video: {video_doc['object_name']}") - # print(json.dumps(result, ensure_ascii=False, indent=2, cls=JSONEncoder)) - except Exception as e: - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - import traceback - traceback.print_exc() # 打印完整的错误堆栈 - - print("Finished processing current batch of videos. Waiting for new videos...") - time.sleep(30) - -# 使用示例 -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - results_collection_name = "douyin_results" - - model_dir = "OpenBMB/MiniCPM-V-2_6" - - system = VideoAnalysisSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name) - system.run() \ No newline at end of file diff --git a/api_history/minicpmv2.6.py b/api_history/minicpmv2.6.py deleted file mode 100644 index 8998110..0000000 --- a/api_history/minicpmv2.6.py +++ /dev/null @@ -1,264 +0,0 @@ -import torch -from PIL import Image -from transformers import AutoModel, AutoTokenizer -from decord import VideoReader, cpu -import json -import re -from datetime import datetime, timedelta -from pymongo import MongoClient -import io -from minio import Minio -import time -import os -from bson import ObjectId - - -# Minio连接模块 -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=True - ) - - def get_video_data(self, bucket, object_name): - response = self.client.get_object(bucket, object_name) - return response.read() - -# 数据库连接模块 -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, results_collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.minio_files_collection = self.db['minio_files'] - self.results_collection = self.db[results_collection_name] - - def get_unprocessed_videos(self): - # 查找 bucket_name 为 'raw-video' 且在结果集合中没有对应 etag 的视频 - processed_etags = set(self.results_collection.distinct('etag')) - return self.minio_files_collection.find({ - 'bucket_name': 'raw', - 'object_name': {'$regex': '/douyin/'}, - 'etag': {'$nin': list(processed_etags)} - }) - - def save_result(self, result): - # 检查是否已存在相同 etag 的结果 - existing_result = self.results_collection.find_one({'etag': result['etag']}) - if existing_result: - print(f"Video with etag {result['etag']} has already been processed. Skipping.") - return - - # 将 ObjectId 转换为字符串 - if 'video_id' in result and isinstance(result['video_id'], ObjectId): - result['video_id'] = str(result['video_id']) - - self.results_collection.insert_one(result) - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return super().default(o) - - -# 视频处理模块 -class VideoProcessor: - def __init__(self, model_dir): - self.model = AutoModel.from_pretrained(model_dir, trust_remote_code=True, - attn_implementation='sdpa', torch_dtype=torch.bfloat16).eval().cuda() - self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) - self.MAX_NUM_FRAMES = 64 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - idxs = [int(i * gap + gap / 2) for i in range(n)] - return [l[i] for i in idxs] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = [i for i in range(0, len(vr), sample_fps)] - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_video(self, video_data, object_name): - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - start_time, end_time = self.extract_time_from_filename(object_name) - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - "start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"), - "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S") - } - - @staticmethod - def extract_time_from_filename(object_name): - # 从 object_name 中提取文件名 - filename = os.path.basename(object_name) - - # 从文件名中提取日期时间部分 - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - # 环境提取 - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - # 改进的人数提取 - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - # 动作和互动提取 - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - # 物体和家具提取 - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 主处理类 -class VideoAnalysisSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, results_collection_name) - self.video_processor = VideoProcessor(model_dir) - - def run(self): - while True: - unprocessed_videos = list(self.db_handler.get_unprocessed_videos()) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 60 seconds before checking again...") - time.sleep(1) - continue - - for video_doc in unprocessed_videos: - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - result = self.video_processor.process_video(video_data, video_doc['object_name']) - - # 添加额外信息到结果中 - result['etag'] = video_doc['etag'] - # result['video_id'] = str(video_doc['_id']) # 将 ObjectId 转换为字符串 - result['bucket_name'] = video_doc['bucket_name'] - result['object_name'] = video_doc['object_name'] - - # 保存结果到 MongoDB - self.db_handler.save_result(result) - - print(f"Processed video: {video_doc['object_name']}") - # print(json.dumps(result, ensure_ascii=False, indent=2, cls=JSONEncoder)) - except Exception as e: - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - import traceback - traceback.print_exc() # 打印完整的错误堆栈 - - print("Finished processing current batch of videos. Waiting for new videos...") - time.sleep(30) - -# 使用示例 -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - results_collection_name = "douyin_results" - - model_dir = "OpenBMB/MiniCPM-V-2_6" - - system = VideoAnalysisSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name) - system.run() \ No newline at end of file diff --git a/api_history/sound.py b/api_history/sound.py deleted file mode 100644 index 113590b..0000000 --- a/api_history/sound.py +++ /dev/null @@ -1,121 +0,0 @@ -import io -import os -import tempfile -import time -from bson import ObjectId -from minio import Minio -from pymongo import MongoClient -import whisper - -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=True - ) - - def get_video_data(self, bucket, object_name): - response = self.client.get_object(bucket, object_name) - data = response.read() - print(f"Read {len(data)} bytes from Minio for {object_name}") - return data - -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.collection = self.db[collection_name] - - def get_unprocessed_videos(self): - return self.collection.find({ - 'bucket_name': 'raw', - 'object_name': {'$regex': r'^douyin/.*/.+\.(mp4|avi|mov|flv)$'}, - 'whisper_transcription': {'$exists': False} - }) - - def update_transcription(self, video_id, transcription): - self.collection.update_one( - {'_id': video_id}, - {'$set': {'whisper_transcription': transcription}} - ) - -class WhisperProcessor: - def __init__(self, model_name, model_path=None): - if model_path: - self.model = whisper.load_model(model_name, download_root=model_path) - else: - self.model = whisper.load_model(model_name) - - def transcribe_audio(self, video_data): - with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video: - temp_video.write(video_data) - temp_video_path = temp_video.name - - try: - result = self.model.transcribe(temp_video_path) - return result["text"] - finally: - os.unlink(temp_video_path) - -class WhisperTranscriptionSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, collection_name, whisper_model_name, whisper_model_path=None): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, collection_name) - self.whisper_processor = WhisperProcessor(whisper_model_name, whisper_model_path) - - def process_video(self, video_doc): - start_time = time.time() - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - transcription = self.whisper_processor.transcribe_audio(video_data) - - self.db_handler.update_transcription(video_doc['_id'], transcription) - - end_time = time.time() - processing_time = end_time - start_time - - print(f"Processed video: {video_doc['object_name']}") - print(f"Processing time: {processing_time:.2f} seconds") - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - - def run(self): - while True: - unprocessed_videos = list(self.db_handler.get_unprocessed_videos()) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 60 seconds before checking again...") - time.sleep(60) - continue - - for video_doc in unprocessed_videos: - self.process_video(video_doc) - - print("Finished processing current batch of videos. Waiting for new videos...") - time.sleep(30) - -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - collection_name = "douyin_results" - - whisper_model_name = "large-v3" # 指定模型名称 - whisper_model_path = "whisper" # 指定模型存放路径 - - system = WhisperTranscriptionSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, collection_name, - whisper_model_name, whisper_model_path) - system.run() \ No newline at end of file diff --git a/api_history/sound_result copy.py b/api_history/sound_result copy.py deleted file mode 100644 index a1666fa..0000000 --- a/api_history/sound_result copy.py +++ /dev/null @@ -1,114 +0,0 @@ -import io -import os -import tempfile -import time -from bson import ObjectId -from minio import Minio -from pymongo import MongoClient -import whisper - -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=True - ) - - def get_video_data(self, bucket, object_name): - response = self.client.get_object(bucket, object_name) - data = response.read() - print(f"Read {len(data)} bytes from Minio for {object_name}") - return data - -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.collection = self.db[collection_name] - - def get_unprocessed_videos(self): - return self.collection.find({ - 'bucket_name': 'raw', - 'object_name': {'$regex': 'douyin/'}, - 'whisper_transcription': {'$exists': False} - }) - - def update_transcription(self, video_id, transcription): - self.collection.update_one( - {'_id': video_id}, - {'$set': {'whisper_transcription': transcription}} - ) - -class WhisperProcessor: - def __init__(self, model_name="large-v3"): - self.model = whisper.load_model(model_name) - - def transcribe_audio(self, video_data): - with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video: - temp_video.write(video_data) - temp_video_path = temp_video.name - - try: - result = self.model.transcribe(temp_video_path) - return result["text"] - finally: - os.unlink(temp_video_path) - -class WhisperTranscriptionSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, collection_name): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, collection_name) - self.whisper_processor = WhisperProcessor() - - def process_video(self, video_doc): - start_time = time.time() - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - transcription = self.whisper_processor.transcribe_audio(video_data) - - self.db_handler.update_transcription(video_doc['_id'], transcription) - - end_time = time.time() - processing_time = end_time - start_time - - print(f"Processed video: {video_doc['object_name']}") - print(f"Processing time: {processing_time:.2f} seconds") - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - - def run(self): - while True: - unprocessed_videos = list(self.db_handler.get_unprocessed_videos()) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 60 seconds before checking again...") - time.sleep(60) - continue - - print(f"Found {len(unprocessed_videos)} videos to process.") - for video_doc in unprocessed_videos: - self.process_video(video_doc) - - print("Finished processing current batch of videos. Checking for more...") - -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - collection_name = "douyin_results" - - system = WhisperTranscriptionSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, collection_name) - system.run() \ No newline at end of file diff --git a/api_history/sound_result.py b/api_history/sound_result.py deleted file mode 100644 index a1666fa..0000000 --- a/api_history/sound_result.py +++ /dev/null @@ -1,114 +0,0 @@ -import io -import os -import tempfile -import time -from bson import ObjectId -from minio import Minio -from pymongo import MongoClient -import whisper - -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=True - ) - - def get_video_data(self, bucket, object_name): - response = self.client.get_object(bucket, object_name) - data = response.read() - print(f"Read {len(data)} bytes from Minio for {object_name}") - return data - -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.collection = self.db[collection_name] - - def get_unprocessed_videos(self): - return self.collection.find({ - 'bucket_name': 'raw', - 'object_name': {'$regex': 'douyin/'}, - 'whisper_transcription': {'$exists': False} - }) - - def update_transcription(self, video_id, transcription): - self.collection.update_one( - {'_id': video_id}, - {'$set': {'whisper_transcription': transcription}} - ) - -class WhisperProcessor: - def __init__(self, model_name="large-v3"): - self.model = whisper.load_model(model_name) - - def transcribe_audio(self, video_data): - with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video: - temp_video.write(video_data) - temp_video_path = temp_video.name - - try: - result = self.model.transcribe(temp_video_path) - return result["text"] - finally: - os.unlink(temp_video_path) - -class WhisperTranscriptionSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, collection_name): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, collection_name) - self.whisper_processor = WhisperProcessor() - - def process_video(self, video_doc): - start_time = time.time() - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - transcription = self.whisper_processor.transcribe_audio(video_data) - - self.db_handler.update_transcription(video_doc['_id'], transcription) - - end_time = time.time() - processing_time = end_time - start_time - - print(f"Processed video: {video_doc['object_name']}") - print(f"Processing time: {processing_time:.2f} seconds") - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - - def run(self): - while True: - unprocessed_videos = list(self.db_handler.get_unprocessed_videos()) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 60 seconds before checking again...") - time.sleep(60) - continue - - print(f"Found {len(unprocessed_videos)} videos to process.") - for video_doc in unprocessed_videos: - self.process_video(video_doc) - - print("Finished processing current batch of videos. Checking for more...") - -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - collection_name = "douyin_results" - - system = WhisperTranscriptionSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, collection_name) - system.run() \ No newline at end of file diff --git a/api_history/video_s3.py b/api_history/video_s3.py deleted file mode 100644 index ec63d48..0000000 --- a/api_history/video_s3.py +++ /dev/null @@ -1,256 +0,0 @@ -import torch -from PIL import Image -from transformers import AutoModel, AutoTokenizer -from decord import VideoReader, cpu -import json -import re -from pymongo import MongoClient -import io -from minio import Minio -import time -from bson import ObjectId -import concurrent.futures -import os - -class MinioHandler: - def __init__(self, endpoint, access_key, secret_key, secure=True): - self.client = Minio( - endpoint, - access_key=access_key, - secret_key=secret_key, - secure=secure - ) - - def list_objects(self, bucket_name, prefix): - objects = self.client.list_objects(bucket_name, prefix=prefix, recursive=True) - return [obj for obj in objects if obj.object_name.lower().endswith(('.mp4', '.avi', '.mov', '.flv'))] - - def get_video_data(self, bucket_name, object_name): - try: - response = self.client.get_object(bucket_name, object_name) - return response.read() - except Exception as e: - print(f"Error retrieving video data for {object_name}: {str(e)}") - return None - -class DatabaseHandler: - def __init__(self, mongo_uri, database_name, results_collection_name): - self.client = MongoClient(mongo_uri) - self.db = self.client[database_name] - self.results_collection = self.db[results_collection_name] - - def get_unprocessed_videos(self, minio_handler, bucket_name='raw', prefix='videoupload/'): - all_objects = minio_handler.list_objects(bucket_name, prefix) - processed_etags = set(self.results_collection.distinct('etag')) - - unprocessed_videos = [ - { - 'bucket_name': bucket_name, - 'object_name': obj.object_name, - 'etag': obj.etag, - 'size': obj.size, - 'last_modified': obj.last_modified - } - for obj in all_objects if obj.etag not in processed_etags - ] - - return unprocessed_videos - - def save_result(self, result): - existing_result = self.results_collection.find_one({'etag': result['etag']}) - if existing_result: - print(f"Video with etag {result['etag']} has already been processed. Skipping.") - return - - if 'video_id' in result and isinstance(result['video_id'], ObjectId): - result['video_id'] = str(result['video_id']) - - self.results_collection.insert_one(result) - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return super().default(o) - -class VideoProcessor: - def __init__(self, model_dir): - self.model = AutoModel.from_pretrained(model_dir, trust_remote_code=True, - attn_implementation='sdpa', torch_dtype=torch.bfloat16).eval().cuda() - self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) - self.MAX_NUM_FRAMES = 12 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_video(self, video_data, object_name): - if not video_data: - raise ValueError(f"Empty video data for {object_name}") - print(f"Processing video: {object_name}, data size: {len(video_data)} bytes") - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - } - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室", "办公"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for action in actions: - if action in answer: - info["actions"].append(action) - - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -class VideoAnalysisSystem: - def __init__(self, minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name): - self.minio_handler = MinioHandler(minio_endpoint, minio_access_key, minio_secret_key) - self.db_handler = DatabaseHandler(mongo_uri, db_name, results_collection_name) - self.video_processor = VideoProcessor(model_dir) - - def process_video(self, video_doc): - start_time = time.time() - try: - video_data = self.minio_handler.get_video_data(video_doc['bucket_name'], video_doc['object_name']) - result = self.video_processor.process_video(video_data, video_doc['object_name']) - - result['etag'] = video_doc['etag'] - result['bucket_name'] = video_doc['bucket_name'] - result['object_name'] = video_doc['object_name'] - - self.db_handler.save_result(result) - - end_time = time.time() - processing_time = end_time - start_time - - print(f"Processed video: {video_doc['object_name']}") - print(f"Processing time: {processing_time:.2f} seconds") - except Exception as e: - end_time = time.time() - processing_time = end_time - start_time - - print(f"Error processing video {video_doc['object_name']}: {str(e)}") - print(f"Processing time (including error): {processing_time:.2f} seconds") - import traceback - traceback.print_exc() - - def run(self): - while True: - unprocessed_videos = self.db_handler.get_unprocessed_videos(self.minio_handler) - - if not unprocessed_videos: - print("No new videos to process. Waiting for 5 seconds before checking again...") - time.sleep(1) - continue - - for video_doc in unprocessed_videos: - self.process_video(video_doc) - - print("Finished processing current batch of videos. Waiting for new videos...") - time.sleep(1) - -if __name__ == "__main__": - minio_endpoint = "api.obscura.work" - minio_access_key = "MnHTAG2NOLyXXIZrwDLp" - minio_secret_key = "WVlmMgww0aRIU43pCJ1XCjubXQO6YsbHysxX2hBf" - - mongo_uri = "mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/minio_mongo" - db_name = "minio_mongo" - results_collection_name = "videoupload_results" - - model_dir = "MiniCPM-V-2_6" - - system = VideoAnalysisSystem(minio_endpoint, minio_access_key, minio_secret_key, - mongo_uri, db_name, model_dir, results_collection_name) - system.run() \ No newline at end of file diff --git a/api_old/__pycache__/config.cpython-310.pyc b/api_old/__pycache__/config.cpython-310.pyc deleted file mode 100644 index 5695d51424b889e08083205508630e58e5f95ffa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1256 zcma)5-Ex~o6b6JOB`07b+?2^E1tB`BjZaEOBnsz4Q0UFzpGw*qTW z`-P#Y+^c@{ppI(LKy_%M2CSndTtn-yfv&+O+JF{p!S#0x+<==o--6pYe*s_S{1x2E z`7Yec`D?hJ^Y*(k+JpzF1>c}8pe?HXZHDhnoh6%+B=O?zcf8o;rJaZSd)+-LEnbBF zJV=`2PL~&Vc6qVa!@M{CU81O@roV_r^U!%Db){a$;4E1IL(&rb<$@49(L|UBp6|e_ z8oJTkiwA8w<=nU9D7O7ca{JT9sSmCf2@BuzU+=!%{u3f=l#cFBw&;u_F(levS54845m&vaYLR|o@~F2<6vJ9 zMCp6Ji>J6RioJ(D!3oB~g*$UYCw5bo6q&L3;o3~PHgV1;jvf2Kc$9LX3**R!7b#1M z%y9QnX^E8e+RI4WrG-&2%{XGQnJDdR_J?L3u`l+m**-KG5K((bM8>)@HjeotNB_H>(mR5&`rpt=0 zs3u8l4YfgTb5m!6OAI5_{7Khz`&3u@nr+B~$Ea!OR{x6d?%l=sHSpPi2{_+XK8Kj1%M?zunGcuVvg1IMrhmd-yKGnoua WHwujvjVUrT`}hCB(tGwENB;xIwP1Sy diff --git a/api_old/__pycache__/config.cpython-311.pyc b/api_old/__pycache__/config.cpython-311.pyc deleted file mode 100644 index 7c609272d04d931511452cc4de3ce55ad08f7f1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1677 zcmaJ=&vV*F6kY+wAP~O{{*g4ssbe>^H3Au@wx{V-HrS!Y76Fr_x@Z(uwkZOs5+pGv z4n6cg=p~1q+Ud0aNiOb84~$No=}9+ca_QvMx03CSU8ma7_ul*VdvEvc?EcI!G=gt% zc5+;tMCdOPTzuhk=k4F%{D3gRl!`=zJtBp@q6hm#FP;#6*e_0CB>M5BIEe#d00+e& zrbXI=6!cB#qL>oIq2Izkc@X*)e)pr$H4S}}1~Xz9M??lk#R!gxQ5+XzI3dRIl$gLt zoWj$uJ$MGsI(81*lf@AOE4;{OR?>Tk}FFW=leBZH|*R(i=KNget z6ETIUI?8-%lg%CsrZ|qv=GPx&^SLaywz|2I+u*E;V?*ofgJ^y&m(8!OXY+*u*p1d3 z4$d?zJU5$tL;jM>aRr-nZF>O*Nmg?w*q&^**}kr7GQQ3lirH6thZ)KWD6d*(Ptw|h zTbGF)4J)e2o@%Q0V*OQC?+w!LnOo5{Qz3%Pb-}kUl`i`jfLnP|S>d);%3ZT(^qcRk z)m=G~RrP-sBZ8*0*uCqEkqJqaySO78ZNaYz{_#GoLT47Fbzt zvByeRG31_N`ACzt4-PKlu|jS6s4Yu9O>Z@=fT7@)DdFdqk2GmdfX4EZwqB@qkOUDo zkmRnco(>dyfso#&-mwG33xRhzmnMvlbZGm}-KJv0n?P2u0}H{Xbj2pG#hPqt9bGmQ zdmf?;Z4i+`Zo0t`h!Thsm?DrOFhgLDz-@qmzuCv~X71ePGt-twGp+C*zq7|nTQy;? zRI^fUv|AGzRjFK*silkraCD1O1Z0H{ulHX4j8>#WKXn?!VXSxi+!n)J13c^rgOELTKA}RoYha? z1wMy|)K`@+j0ccI!EYLXV=qNfV?UzkVUP@PV@t#EXf}O@QV>MaLwdv{0OILkWE6`J znNc(bg4FaGO8m(zl4$@^B0Thb7yACIACqTj`FFJZ2U-SaghDsbpr2xg(NSWRz{5+A zgfkD%P?D^@3Uz5TJ3mUM#uHw~KSl&@WENaalxP4zbAObahO8OF0048x)r1KH0L+rB zNfHL&#>)R)ebLQIkSqYmy6ZAA!T{`1nS^JI0EUtAGYZA$;M8KXLxv2a0B8RX+Bn`( diff --git a/api_old/__pycache__/cpm_api.cpython-311.pyc b/api_old/__pycache__/cpm_api.cpython-311.pyc deleted file mode 100644 index f6ee1af029e15a26d10ff546af3414b89ff164e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20509 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 IQKA|D8!OF&i2wiq diff --git a/api_old/__pycache__/fall_api.cpython-311.pyc b/api_old/__pycache__/fall_api.cpython-311.pyc deleted file mode 100644 index 860bf63703a88ba8a0f88749d79505c0916c0eeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16923 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 QNJ!_pj-YCwKP-m+H*>q*i2wiq diff --git a/api_old/__pycache__/pose.cpython-311.pyc b/api_old/__pycache__/pose.cpython-311.pyc deleted file mode 100644 index bce79c2d6b4e4a9581527a4592d64d32c66a9128..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17853 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 E0{aq!`Tzg` diff --git a/api_old/__pycache__/qwenvl_api.cpython-311.pyc b/api_old/__pycache__/qwenvl_api.cpython-311.pyc deleted file mode 100644 index 3b43c1018ce5c88619424a33c6cedb04eda4c4d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21144 zcmdUXd300vz308!SId&+MYh2*#*1vQ!2tpW4909`P1stLMI-ylAdqF}N@8*3)JaJr zTKZymq&SIboIa8`jY<5vyi%I9q@CB7GjrygE1x@Ky?cfkW{L^So0;cKl9^0q{rrAc zJElqJ^q;x@>HFLF<-7d8Kfh%*8yI}QI{f#&Cm&*%KM+sq(Fb!+={Ze5?=t*7CtfT7RmHd41bVCplw z&D5<46!cl#mcBxFA@yqm);^ot)>q^%>a)A;G)xyL?kjPZP`5r%+E?Z->vOmr)Ncrs z_f@znsM{E*>|5YoK;3L$p?e{1XO+8(+D^9!aQU8too z6_#!Lj6N4v$XSoD`*5B%iMy&9<&_*#6+e9L{UlQnOu5Q|?jb`4@}$J$bUUlX^O ztDUTSQ;AggB~z{C>fP(O2KRcTwr8jIt@Ev)bmgUPS1{WcuJH`RHRbl#y#e_*_}2Q` z^TIcBOME(Rsjs6~#WkN%xi|T0$2(KCXWLv$?iipvJ+~}3td}LF^(wgKXViJCaPtGp zZOtv?kCyw;edTU>V7V*)gmPEjSMFAB)fvXU4W)0-9)*;fdpNgrZZ$?w!L2!?%IkL* z*M@R-_;zw@kJCNHlbNVFwkuVVZyQ$Ly1ZH0?b|)RCzT@G&MiH;@Ata*qRhR=9!aJ2 zE#}s9?UNgFR|itF`{~cM@g7OK$EA>ToHy(X`}=&7o3U@rxC7HJ$ zIPl1pQ$4@AI_`p62{5ef^k)5Usz34$RU+yOy?YTG|y1h51NNc(ACo zwY7Qq>Q&9lmo>Mpe5h^BvNf%eMsBZUl5NiszCSWBSdg34{|{y8jcKpkfsL)pT36g7 z*m1D6<>bkez}Y--`b*uqfVZ!k^S1K@7q;UUsstEj-mqUOpXhjR`;WIzeIvePy|`q3 zyt-YiZXaeoMLgqD^VP5<9i2&!pTnxcviJ1&_J(|64+hj1mTbPDw>#kT(3qb7zCJ&~ zPIvsE7W_%xgbPz!vdJ;UUlbZ$LDb@zoDI6bD#C>8C- zUiNn2&^R6PhxVTig?)WJX(Wo;NmJ;$Sw@=@WuuG-F8PZp;7hL_j>M&!4kD1Qi+hB+ z#Hvu!%_-@V=S@E^73}W#rf1K=-JY%cI(Bc_PgYLI7wF~5MDrT}LZn`&)5*JFOF9n@ zCx6)E8LXVA&CRLU10>@@fMF(4Tpm-MGb9{~U=<`5I%6tPS0PZtHz7$+y0P$(-Ap_B z@6jR9x)|#r#RIlU-gX#LhSM|{!$g(2+ha%-)}rj>7gdd`QtoU!ch)$?ppjwn%29_A zmiofxl#2ni9kyhAQB`ifs5%#$<;D(aa`I|&W24M5GA{HxPTS2}6%3|blO2=eYEI}$@$taB!+-F#noxHOZKQ|I(To7;=r_OU1pOH;EaXZlI! zitzii8{h@z1x4-|c{Xn{qAZ!9?R-#{sPL2g5kd z6wS*d)-NfKcn2gl5F-6{I){hpCi~K`E&$Ym<8YvTaMyg>%oilbIj9AY2hHmO{XM|Y z?ah&(zqg<7^Mt&80|8$qJX6}^s3rvVlKEno5sEg-W~zFmd~H;>&H7Ms$bt0 zFK>#MED>SsOVi2dUd73&4ETUocyAC?h?3ul!X!4x?;s_UvHvH|jq*uRklUQW)&H*@ zA*XMMa6@09)i-U$Bi{3M7wblspKpwHCQ_Y{oB8v|wMYN@)Xz@A7cbfnB;7`hS==6&O*!V1JH8b+~=+Y%zq=9e@Pv|%mXv?q(VvG%lG$r zdER?k(tuP2s&Xm#?Ieah5hCqO9=JSNiGwS?{@7(E+lQQ?W&q5!b&G7SSzoHX*fL)I zR>O}PCLWG2dPrRKP~5gcw5>=dq2o1Z3`Y8#y}Y;27ix2vB!$0U(r!8(_Jz9k@>Z0I z70@3Ea+2`~aJl!y5l`lhYy%zN zK~fm#I`)8)_ws};N(B&VO;ViTC4;=8X&o3iGz#RzDM=%@FGO}N5ET97wP(uSAcM}Y z8ok3PAao00n3+>3^d0ORlYyV>n3A%K?r}rBxJfK-iWzR2%SOxN=Eb6UaZGWuxO}wr zwf0xq;}uO}Mbm^gUeOXSUM3bVLqdDm<>Jw&-l!Sxe!c!m{X|>bu}XBTirXI+?GMNF zH;o0c(`VbywU2HVD_f?GErPKnQC2rvFn)S!vDmsLUba;%+ZyXiSc@)IUZ@=P$E^*b zwPD7(Y}&dkZfzBRJwvA)au$4g#*Z0xbP&L!$xg3gt& z6vxbTZY8y!un;}u%t?I&1yHnm6z~!gWkbc;n*cP0@>PhJqKa3U%Xx_HIX3&u<&-p~ zilhWO$VSL{r!nF+m-A2rkb;uaCK=62(7{=!d8(;E?Rg~)sW@#^#pyWxm~u!RYzk+g zy<_PXWTWb1X;L7+tlZ5zq~Q!v4QCuv45>L&RLz;Wf~*!QI7^V_3d!-$St+!QDUK+( zB0zRXn~j_wfQq%8ohzO!$t4Yjbm8Kx-sX+b<2W6L^g{-WPMRplq{iszj<8XE)W8*U zrITg3qNyQc)Ht3+zGU)oj=Y{5L0~Y)OsN#d(u5AT$j?NLS6Hr`tMIc|*uP+hOkCv| zCcGe(7Hu#dOOrxTQ!e7q$HGJAoN+45l#+2p&FGCCf- z^MrdtmS{oL60Xf8JVs7r`sETmNzqU18R$u;r}!yxkIosBImJixvVY z)a)75Z0Q>apLPx!oaFd(0)IOPjY-jfKX}ABsAzKznwQA2A@D-*YMkBVB$m|l+&m&T zZf*l1-3cP$Nrh5n|zJ2Y-5e?Gbed{~-esrEXXJ7sG$7f%N5K7`7 z-#<5d?)f|4o17iHe&^-){xJOP?d#V+KKu5am!|H$@FKi-Uq5&I+V$BNM`zFd;Nz=j z|1kVqszt|<9R!7n@5ec>r2>p#cB31_iB#<6IM0!^B*g>91p_822EyPkNdc0E zo@U^2>1c!`?Etc1=%fPP7mn~jPuLfPlqJNkB&iDsuNU;4^w1hYe!^MhkOZWslqA>$ zN0KC{qA3V9XKN2R69R5m1 z_bcZ^SSb-vA7K^eBksPB28UJy0E;n-YC{J6ecj6O-iafzZ7+3>YR>PxxN~IJ$S%=T zJ*Uh`mZI>ALdX3OWDNLvzQ6fs!Ki~`Uhlu|h1Q2!=;;?~%z-dp`EM+X2Y%=Gp zvBB!y_e&+3bCUQ`R;U|W2yDK^F~_j_o$O6prLdrD{1^btwVkpZx9t#ZI|S2?S)ExZ zcx-eB0L(;_Z2xWNM}`mg{?om(4=}EKT+}@-=pMgiEFLYLHaZ2PGg0iADXyC?t{WeW z7q^PVt+B0%+9gvbZuAJ1J78kFW4mYVj*B}k^J9Y(jq$3LV%5sHeU)fm7285mMuf`s zFtOcpI>u3T%U+#WP$MioB=iml;isp2pB7-;-7s?uvq`Z-@d?8;?^M9EN71Pyo=zn^ z#hX)5t*o@osWs+;IfekG&Tfe5=Il(N{oFG%mgUoyfrPdA(!vW16Sk6(l0;DzG4`@VMb(^bP~WXk834u&ilRHD(aYnuM}0n2BE5zOi4n0b^#+w#ifB=9+ca z4vFh`e>4N-i#(u`ZdmE=icGr^15UgKpTCA{l83 zz%7IEGqu8IMvHVz`NA1T{InGS zG%GU>`~DBIGDrp{6}hwnPG;pEiYeb_d2({Xx&#ZSG~r43CkZ$QkaIjs=^a9M@eoVx#Q*ygRc*#2z*jDN5W|)^XPn9qAIG4 zs(KasnHnZskQzOPSqxOjGT|(Gm(xXAa8&xsd16nb%I1dB?p#XuYo$qZ0N{4!ZEdfj zM{yYP=9A1RoV+-SP!KQsQl$iwt3K8d$_i*mh;qZuiag{pI?tYK{lcd0pSL@ zP=rw2cP@?J`RPlyfAtq2OK<<`Z)Qin@2rp1A)?-S_pQIW^TUxlm;UD7rJsS2y#2v% zK)~Mn)#%-KucQNA21(HmS;T-hd{okPdqcQl047$41_J)DjKm54#(f|z?^Gz3A0~j( z7iZxDT}k$Zq$A=7npH=VdXNW#2#6>^R=|%7o*+kEA#xB=R4QZ9@)XrTvk^HDy>}u- z4K~k*QXicDEtDMk5@48_Eh@hF@Jvz7bWzRt&Un!>v1nOLmnf>Z)PA9Tye3{$Cl=L> zKP48qCOYFqEmIA@X#Pp_jiPw#7Dy6qc*WN3Gp&23Tlahvins0;TlWhG4vMXZ;zfsm zuv8lT4i=}hh|_!Zlt(_DV>=S1)o*l7bqkBv#22p-0ZZ4!wkL`fj6%FtFPQ4TxJgjH z0(rCHO_Noy9iAuykWJj=7ENx!^i7K-B9)vD(H^vEu*CH*;DGAfYGC}s76J@DTmMW@3Ssx6k zv&A3BP`~^|)i}i9%V?rNBCp}EL^TgMT-3P?_&8ab_kFpo@yam{^QK)~0o$(B2UsT%1#{2KF6 zgn7`xw!_xLE{1J@T>{$})p4c0im09|12k|BKqFV)tLP>3=AtZ}3Bm^xeAN&iK!AXL zRe)SDXCVUZyzS2uDX`4BMduo$&`xkw>~U!%BW~XPe1EVHm*+B5GvMooNFD;v9-0v**Yh;Ya7=>YE>5d@XwK^FEsHyYBn!WNCwVt1Z<%%p$wipejO_(|_x8!}AkWBm3i?dhXcFC6| zQ%LPn(8VUtLWG>KaPnJW-y@!T&+n1V!joIydt~j($XzdIyM8X2{VcVy zfRf=CW&iV6lkwk9`rncLzq~B_zyBSrJS<=@ck^1jCoo&C#aPt~@_`bwp0``Z?mH z$Hs*p%)a#co!8G%pG-&Hd2LJ{g6rRvlmF!9*%#0Ae~pUypAhg<0)7VI(hxEkTH4WTIOjUbUdV?q6{#umCi)@!bGVfQAwerV$Q5B1hWbdQ_b0!vaPI8 zx&2;6~D78|l3ixDYrWyciTLi(wLVt`XJ96Y;{DL}BrWdQQz0*3Rh+ z6(}zSVwSlQrmTLBF_VG-V%u)oOD=Ye9vMFow>OLSW=JqB_BqCAhHRkh(%K7aN1r0J z)J&0Uy2uqTY7&c@<`}(oAA8faWa8j^kNxJUOi!*T7K=(M3qx^+~DWbN*C#*t3i}o74Zh-7=9Cy@*Dv~ zlEA-8z-s`K32Z~y-y`eJ@mEPWS(N;@2q1hjWgPgm#6`e<{6bd%hSPs@3Z)v|NdrTH z8k3O*&Q-*l3kFzLy%)537Sv?9nh@~>exgLcna7hsWi)n$8Ve5~9hVBA zp>hXEvn3V1vDFhuMV>jT9Xh1=Hh!P}1edR2u^V8J0SW>0Z=2E>Jo1yWBpv#p_M;{|+G5vDe@hUDSas8=;Cei$Dn3q-z z7p6)B-K)x_XTduV?k4%+dQq7hzmg$5!~FU=QAKW#s4gu=Dz^ks%ElUIITKQyRLD#@ z%N)koLo7h(m*h82FxGuvxI7b-eYux)wWk6_t{Tg0$#_4O=I7NYrWy2T*FfQXBN*77m zq=KrXIN6PxthbR}zF=Dn*Oxk?*a%J0HsDQc`Uvi&o{IP)K5nqaDJ!A+IHxzrIpt7# z;g=Mt;M$L$zYZ22psmNx)!_dw(rL}Wl}#)V!4nw4q-S3u{p-%oU$ zAKA6HW3y*-*FOGPl16=!M-FIBrWBZ=;YU&RzULifS*r)ODeY++qm@Re5H3+pMEvtaQfV zns&G*7EdmXJ64O1)z_?|W38Yn0b(m$ciBEx{&MA5rC?br)UP4XvUYg;%|hEn?IqI% z(`ENWS-h}SENmU_x~VtE{G;}`exayeNDflRcy&Uje^L8g?T9w6D-(5Pg05^%i3l1? z!6zi$X9Q%z=5s-|F<9ve*9D-(qd;NB(;PX8v&Cydgm{fvPB zreSmi&z*=JdG=dpzV(S#sV+mXx{Tn6vN;1|Umz4NgBd?6+vts|a7JaHR@p}mzoEKf znAkpbRA|}|cWxA&8{?`DQPm-+I&PKM%#=4zmp8}Dmx<-egwlga({o%f28dAwLS#e! z)?Tt{mGZZ%RGZqgzg^D)Lg!pw;mtX+8FDJpbN}DKA)ottqFMl-{EcI7@Bi`zyF7&W zP_|G&JBu(J$uCNjL2w#pq%7Ikf{IPkr#TgyHUfhX3OV^fXOqZ|l3)Ir>c{CenJ2g< zB2EsX-e+(o$o5G#E%4Orq4a`$1F!axi_3dN`u(;--XTW$Oz==;Y>i|}UIS8OnG`OD zPWr<~>B5)v>30h}(a*sS4CA#yX4grqFZXuBn5E{73vtWO8WZTHLkLsb8R9rp2ctS7VunCsde4}gq;-(P<0gIxt z;Y~}$_&2WhOjW<#JJ~BNUU}mo!LluG*(O@H3A%iP#s3A_H`}R*uL3PR^U!MMnxSKf z@X`)qsNqEqK)sI6auuc$|NE{RoAmmRHU#J(P&QVu@s-b4b=3bC(#1 zgDk|Jf9H=SqMXb_>&Yd;K0}_XFhj7jmWW!ub>jaK&E-M3GW;h1Z>#8(^PiIdGTHop zC7}v=vi}?0*-C+UjFCMa{QpEe9`iAuJ_AQ;s+AaAc`miGLYiP%mTz>+{@io$1}g zvB4$ThhaunE<4ryBHaArIhVL2_jTE~W6+$zKFKDzvv0F9%I))O$%VdfSzV@;R2S56IloQ% zoYB0CmuGV1t}R?*9OR1rX#AEx+n%xi zv6geiUo|>GOU^iyV-k_22#?K=NJJW z)YicCk@EQ;01TE=?KEemDreXmI^O1_Y!Niny?i=S*G;BfCS=fl{QeO`*RL!VAr?H`s$aZTeD9QgQyCFy-KQvg`PI z0@55KPv~y`-x6?#fR72dO~A7RNB~kXw8fyNMyU!sKqD^!AyUnfU>Wvp*?(}?0m4(F zI&g_Hr9v;w38f-w~z@hlx159WUkRhIj6jjVI4eFijCltoCbBcPRlV)*@ z){iZ{YL8o*MN2byBy$IwC|!7Q2!dJFY-z>C!SVVlOJ^3XnqIVO>STP;2654bcc_w!XZ5Z2O2FgtEDoMo<6-(_9M%GdnC3n{mku=KZO_1Up8zOkuqa&ITyecRkJu+>r6|A-MrT~-bTr_s_wdgC+t2?f( z7FO?&O}uKSShZ6yRo=7~3ng_E%BdrQwLNZa7p?82VV&%a>YIi3i~7;J@#?AC8>~>c zDPFiqEZhVK>1HQkDbkSiw38)W%?-~^lkHCSc)rAV#bI`2&qboVHc{>bW4!{}djMdp zT^4ZAE(=`dLb%L@WStaJ?-!pNFltcq!(z&YRNG<2)y^sQ?X8nr1w5m%t%#deil&u< zX(d!5FCTjOn`7S;EVVEbO&^r}vh4jbp|JxdQPm(+u7jCM+9Ud#g_Sdf&gnwuxZ~>P z$sIQu{^h}M3WweCgO7;^9}^ya945Zx32_N-5}p(bpA@K>EyPH7#S3?cg*$}89fYy^ z?*z=5Nn43y3rjYHye&fA*iz>@_1m0^-xU{ku2TQ5(L~&Qj+NRHL9)k`rp@+M89ujiM#0`xc|;+*r8MZUE`*r9ZK~-Q>x(^w6(}P zy5;GV7R?Yn4RYvx71}~20}a@~GV8{%xQ!no)oMN6cV+4<#i z&OMV&>=2!bNnwqk>i_3uW)6SUsSh`yhunMzdo+ne~~0B+(!kT&R~BS z(oDPwLrMPyg^)+*1e-yhisGFQNx?+SC?hEiT!^CLQ;H%ecA(h6n8XGIN#`q98OUog zIi4ry{$~6_L=Gvly|;>Shh|jQP!Q86N}Ymz1-tbPB3YS)md=OMgV}Mfb~UbXwjoU%fNlM5cGrMGO%&*)*e= z4u&J2p~F+G2Ry&ZEfM?mKt8+2DU_9{%9BEPyh=Ih4F-Jy zN@ae@w%z+b_y)~8eW#ry=7^7LbGo#2flHc!NOvgG4Z0IB)WiF`ad}Q|sqsdEl4Nh8 zQ&i6oLcA(WAtjage~!>xF`iw{gho;EJFO?gjy6;G>x5|+nRt)i+`P_^DFa$H(}Vg30H7dH^$u!{m*v>I(AtIlm0JvMD@ z5R458n`6`^+7{2)nx<_{6Wiohp)DncCrhQ(#cJY9y)I!gPUtfQ4P`j*|{`sGP@DC@(GBP%V zr*SNhUl_35Li!YWx=uo-8Y#DBkjftC!5=?#FW? z;E9AmrpvEN=>OK>Ir=J=C%m+ze57la{6Ixbr;sOHtNcvm0}`*M|DXbQDe}K;3HZDD zViK+g-HXc}@^(oHl?us_ylEnC{NsiQxDXZjFBgy$;}Depv4v#m*tKi#H@0l{?A_P3 zt!vMIsertg%8Y?z*u8i2mR+7lIu2}?O!Ae_rhR*NZrR8GfmB)`2W{K8_uwO*uFVoC z{motbJ=^#0Kag@ivUlGBsXz{Yq+|d7Z|vQ-S<=b=&6{M_!h)44naKZ=*xI#gi>G7T z7D?CVJ&qNOQT%tL{|2OxH;RSo#17zj7LVoOd+iae<1RDp>WucVFOKZEr1 zK}pF+f_ZoG_^&7OC4W2Lg;HUd&_94)0gcMC2}VCm{}POGnEqus8U$t@lVH{fUu6ga?}hvsB1638qGv&m@>)A=f0BQX$u5bw520reXRwUxNz; zCTVVI%)>{X^`G$%D-&jW>>FnfpF2EkNHEG5^v~&It`T>faf*ynV4Mj?ea0}u6i+jF z?H^~#M5at&%5D`k3$~VF%PmGZ!`P-7TS8?^*s2pH<%yz_ky;AyScWaaV;NR~$1*H* zLI|)i1#3}}W$~~nQBX2$OjxUi3vQVgAk1t9Al`)0G_1R2?jT_VSju9jN1vjPG^#~Q z^{@$-hpcH>amEV)_x;RrOGIel}$)6_}=}nH<`iWf=?Sio_Zfp~cZNs{Ru`ISe zp|T`OYes?zb48-WnJBGDl(q|{?a(nY;mr}iX9Nt>r$oiENJ3vk3=(TCb9!|l(g1u$ z0GefE^s5D3y{LOQp(+^NI&lh*dGtsEfLA(t#jqMGEd`~dUnKx0R}6foYohT+x$w!EY4)d5}~YVV$0N;c-eZfY(19FoT`ZJV@KU{ z46&y=WqTgj$S6!NR6SQU;)t^qB3qGgEuFAW^t|nu1e>L1$=woxqdT&FvKx9Rib`ae zQ_yq;$&v(Xb(~!!vWpT;EfXuIly9$@T!ZIDv?u~ck7fI0Hz}$HS>_Zpo$1e5l#3-y zR~9_PlF}DGw@@ft9A|4qwsuZYz;0$oLr~;_oefF5>e4CZoLUx$H4SWx$`l)%RyqWw z<5o%Ch+hP+9k{VVDA^t_*)EoBAKsf#70sv|(<(1XaZ?m2G$r`NQfP-gai^ dh;kHH%~VA|#5GWLZB-{D}HcGAUUy90dV6LkSc}&;!tt z7$9aGtqptC4Xw&Yj4iKmww9M_6{lZgrZYX5H=C(zVli zecEpCogpw7kV@=qd)mR^&YSn{`@eU8@6PYrY-R$kKY!sbMwwQE_&L55F5^nz)2~qk zag(44nv4_UWSAV+gf-*Zuy&jZQzXV~;<|BtSU+wE8>Dw_+&FFuo1`!mH;-Gw7AdTY zTf?1U)Tpv zW86RP5BtZf!__Nz>%!}#^qO!DQ!`du=uU2ZAyoRLP4AKf@qPHq*Gd^1!W%RML%s`< z-!CK);lLVkbxa^vUqmSh&}OFYT@B=izkEU2mT&{$Hx^<_pNxrVpslp+Jc<9i_IGuV z7yj}Ec^%;<+8N$RSA;jwu5dG584l9!a0~4TZ>Folt+Y41h4zKpppDhE>8@YhuW$(B zePde-)q<|_LbqZJqE|cY=joBE307}UIs$N6>+*1YI_E;SR{x!8DcC*-6)5CBi%C27o*1 zMu5ARy4(|m@=Bj{SzahlOK(({H%ii*7;02QH(%A2&HZjD_sP=o3Sl~^E`x4i_CN`n zuTo_t?0swrt!tLBWmO6L=r-u#emVs30MkWpy(q0M_8jzO-PplGi$oU zEq$t6ND^h`9QZqw;~eWQ)Wihnc6$5Wj(4>%*YKAwSkby6=^yO;7^8xeXy}b5Q(Y(f zMcd(1r%v`<9$_X^vBZQ(^~U0iXg)m|Pef@92Q^~FP>N-uye=o z1BJIBDcTR67#untqv<$vDatYsLp^ii*ogtiZ95try%;^oCg`aVhFy6*1f@)kGb|wL z1{pe*1VkepO);t1I3t=6(o8%R{TRu`v@Ucd5l>9}FQihF$=#vQFq4Y5C5DqDQ*5;D zQi8pBN+esLQ?_V4p12fAu(9*8iKJK|zmBj_)r)f-OzHbew917GN-QlndT1+^kImV_0k!)jQW7ok$G)nUriP5ltrc#MW zVstdgq#`h!OiFYy6Vc%~6OmFz6651BhqB1p;gXq8THR*hDN9iA;M+8`M^aJ_<-l4}ckB$>X1E;XO@)rzuNaGiTw) zHbKhTjYsBrlYm79&(&9XM>3kUW{6<70cMb7+wq}gx5Go!jj~aoCkA;dB$GHYfkNwH zh2a2BSE|}BM-BjD5;iWhJnyQy*>b&Q?lkXe5L^veL*7x9wabmi@(LFYoHX*k3#0n7 z<|c8T$Y|4KH*xO643(zF3V1|Dm)1e-4>S6-UfDr3=`Uiey_eK11VzP+A!E##GUluX z*s12KF>OejMm60;8752_V45T>Enpeb=Eq8PQ zrnRtxbOS7|<5hI75+XkZ>700-#Q94T=bRZbO}?U`38e+oKoMzj8hZVj=2`{PFxClK zKAwRh@4(U%HEmnjdWaG2GAD}0BgqTVNoHmF%}AT)3-g{h5Mv-All!J6`c>G^ZKDZx zJSq_!px?>1J@Lc{@UDFjC}ap@JDUM~K0|Q+!}5K17<$Lh}7_K;&RX|>fpg7Me6dE%U48wDuH7pQiv#0u@o~dYGV^AQ9BY( zN|Zs=O#npz_7a_(WG3jKT_P0hF02e&FILE-pJHgF_&5O~$+CTzq=l+P&E;*P<_i2T zx5KlYJ%TxpV=^U`Y=>YV@C~3cg-fQDcJ-`IuTxPSb>bkDk+i{V!Sv>9)^kRGzQVUm zRG2CyknPI5JvaKYJ$Y}UzOL~d-+bLW>lW*_FVt=4>pF$H&e!a-n%RSM znz@6^dZN0YEG~%$ng<8(UxruSe^~Gzeoc>f`NCp?_%#t8daG|-zL~k6dF_Q8FU(R) zRsOk#Tbtf)eWUezZMWL^s-RF6gqC>M-Ix}7{V3u3TwtDUnr6?GibqGP;aDV*osjvfVRAc-NM^n^L%URxX8DrpYvw){Sb?`m_PG z5{lN9>y>Hao1l58O|;=n>Md=?tSYBSYSZRaF-#C+#ky!Cln5z`4)--MuFUPDKv)3=SRcc_uQ{fBfXJ zp0lE1a!Nue5P_Z83Y2n1y}b3=A&fbN;534#5u8DQyB;Lf=r9u(%_*oRaw$fqE{GN> ze1VCbzmO8iNK&Q-1xkQVnHpGWHhQT*V5a>lT2PvF0qRNqF91LYsx~bXCR2ozVYYXv zHgLN#H#-0HI}?0ur%>DZ%0Tv@yxH@W{<%#z_Gh~>&?;1J%631pI<6g`Ys%Hx8QX5QMA4bQF1J-4KkJpUxWamW1%e&cSg?#cVZ4|f0a#78W*caS?3;i4nFl@_ct zXQeU6(K+4RRF0i*zenELk&FM6Lqf-~kD}aDSPgIex?ue}C*2Pn?yObX`U5cEAaNp< zjYLFqBmz?D6o}#x(He<7Hx-S`DQqWB&=UyGA{aw}d`c=r8h$o}uLy7+lQ`uw#ow}q z)NNcQia@W?d6$VIFuHYh%R~{hT6LafVl|K}!zxq~F763QNIVPhCSiJvI;Gly%3dZ^ zdyay#q)Sc7QiW)ED%3-bgA%hMsHrKYB7mhessfpxsQw#$|7-iKp!Ilv}QIi1q7?pRhuq0v=qIs}q z==8Buk?#IM(GVNI#DbhGniLzLZjzN`X0Q$gEuuL#9z74%jc6(=8qpesWLC5mRh!uO zd0BGSVh9Ck(Tawg0(S$O+r&g71-3mp0vMu8c`cS^SK_30l1vi!d z@%HtAoz*^aRo@I<59Jzo*G9p$F>83_-LU9wTky8cU%GGSy?uhWFKd0|3oQD!Ecmv} z$L}BGef@&3KWlqf8_J#KYD0J%tC#%iZh7CXd86j`7}ws-`+Ed`Pu9LQcX(Ew4-dqoT-gUYR;{Ml~CGQmZw6Mr^xk{9I*Z?fZwWiOU9Mf zuPqlqi(Rz-R@vHyJy6;*WTdOEv?J{*tEDn+d(4ikRQCep2pz4PhJ6O6(=Rn2_i#Rn zl1XV})bjx)+rAv0O6DoboaMV*l6WjD5>JOxuO#ufOJy9|kU|w@p5ee&1|kZMq7pb}s@6Lpf@APYs-r@<=={^pwRO?&OT{5oCY zLy@3l%o|X=boNprL@B{CaZ2hE`_CYYlwVAUvFKZ!5N|(SVi1`@; zC2p+{iW+NR{ZdajTqtzCz}rKDJp}Sql{ah2d;K6X*(+unZf?51>8%E#x`lUb7F?To zd#hk?1yRjeIeX}4-}Sz?4hc0Kyt`9yck<31f^$b!w^ZSt9lCku`kA+egxVdvXQ$xV z$ye+WDt2Y{`RYKfX>LQ-UM6}~`EH!Q8NVLCy_*Yl@l^+fs)JbzN|1@Ui|=h0Hg`SP z!EHYBQ5SdeGvT&e62g)OnBLoAwAzdpKwPqw4y%LvMt*mOXs+ zUZHyLtYN8X%VN`>g{D3C&pepmn@$T&rvWeTtXXt6EI1qHFLM3Qa*bbun|GcQoaZ>_ zxkvRai}gDf>UZAPar>X*>j#DU!C6P%S$WfR-82{Coh^d1g>$w%w0h^Z@z$E`(d9ar z0#zrL8wr#3yOviiuh_2HvNjNXm6u0G^EK0Kh&R>?#(K_J|9?jqMW2xuclEauKdkDi z(f-J3h2W2BsIGwFM*%H{8%PW{9=5^D&)UsLHfn#iXTy;??H|_ZAgnqJLfZ=3T|PY^ zS(a7QA7bPyO{5Azc#ffdDP3iJj#TAVtU2T=`v_QNa#-`KYn6ROTYBiGSaQH>0IgGe ztiX;^lyabjMGH>3O=4{+RLv{#*b?j`AT{E_A;7T; zS|sMdqA8mFCW17A41y^H-$L*Lf*AzgM(`qlpiw4}CYcVh=Z8hxN{~r{G7W4%el6~)0z)Tp>;Y+dmC4Dy(_%;3BmgWN4b~0e!;t$Yu&?p z_X^&<7*pvJDjT@Q4!*KesO(& zo+vgE0;v+K+$u{Pl`4^=!DhHx*MyytD{5Dz<(7LcNIsBXMb|8mX>%pLSL#UBl!P}Z z;jy9?lh%TMtbHE#AWcn>v~IOLzqTer8`SfqrgG2|I46@vTAylE>dmcfsnn;|Y#VK& z&9O2!mGXL0L8Wxs0%vp9TbjEzb=!dE*asx(m%RztceK+~+t}p!X&pf3JiJU!oELSH z5lPfZ!VgHvl89%*O%h36nPgb>V*Nt$v{^A_?H2V(uwzZZ=`onxrWks9Q!ii#WKS)I z4n!yDfSeM51Kq%iAPRnS;7MbEQ|BZat+1Lqq-a1=1a4_T4|^M86nR@TCMV+{@Lo>I zEdqb2OCbGHQ&ZrPq(j&tiRA1XnC(pjw*Y{TQNhIyoOUK?(Q>q__h?t-)QOY*hgckK zQAdO25E>8X=ipf-TEQo4IqLp*f* zZuVdAf9>dvqnv$1**(*@AB@HFMDMM*^wIn0Qbx>5FA0AT``y4Yvuj>1^<4Is#-FE^$VPZdDAAr1gC7I z&Og5`Z!~?^@Ty_fz#Dyn(Z?BmAjRtZQmO<%iq-i+iY<*(Qk9S_#je8orIFCtUOxWf z@z*!MmAW;ZcL(x;Hm?0B7r=Y=%bWG+!S|c04jHr`7^p)Q!v}T};Iu0w zACQK=i4%`&AtNnT6?Z{8FL9S2KtSd$VCx|y8bOvyPPw8)1&GC~oL1tQ72Bvf7bCWuWA;mnn77{tjitc99wzER>85AGj4rkUpKGiI(j*KA8+py?0uZE zPnIJ*p`wOaGV7`d$IUKHxD&*Qmks}irhMVcPPy7`qi9t70mM^9iwzv^rAk=7azL8X zL+e*vsiN@^3}uvhiM(ofOi3l>D+Rt%#y;86tO30CbXSdVAXt*4q(vo5SShDmbf@)7 z-_{n&)dCS1pi2yN8ZfP?)6;s|0K$O{*twq8)5LF;xWeGp1(CsYx6EJ&+ev8ze0l-S z>^AjS!FB?MEN!QsLSIA^`n|=$|B{yMAh?o?7f!@>_w`KUng63UaCDCHZ@qHDoJvbzR$%k10XzfT&@I!R?qrr&sVNMGg!6TNVuBv5Rn2 zR^)cFH>~XH2DT`$Qeo0`BW~H^c9Hm{;>B0M`Wq-W`S$^QF7M$bKWx}}n)7ex{o4iq zc3>c$E%&bE8(KNv8aFT_&lU+s0>F$sTYwpr#wn>~A(5ltCs&0DHwFAJ7H?&yLg#92c5x>jyWmr!?*v#wwARo^na zZGFRfJIrl6#QVAhUw5{D$x(6b(s#f0>bHPyJDLRt7}RvOh-O)%sep-I4M3WyZ+yqQ zShsDVZW|u{)%6Hv4NiDgi4$m1G# z3@Z=rz`2jsb7!C7&W7O@z(RE3oO}b`kml=%?u%$Xk>F<%gqVtm=8@T(JH%~0!kLfq z=A(l7C`TQY7=~&~qcWt0|EK>0Abdjr!NS!|%#!E+D{fxp$OnCHOVvSO{ovoMkQiX^ z$Rk%S#RYF|<4MX~z{I0LHQB&pxfKObS9<7{!$r30O6y7w!xUE+boTpoYKw^gKBQkc z=s8)YLY6sB05L-o%bF^rnp$TJU-7U~8AJknrOQS`I-UZL@`)4w&)T<68T7}uFKuKy z(k8b3H=nVz5%`;lHr*;Sn5kC>9QuZS^E%MA^4G13S@PNpILDMI?Qm3d9-L!J)MMI` zsw)|lFX0^XcWATuSJj28wp!6HAFqO24)p6km%HUi?A10P)9jx>k4oeLv!W}rA7Iii z5TKL`UZ1lVMzKLYGtrNNhz-iOw})j_T6Q26O9)B(VP*3`xrMZ4AAR0MAFtPl+t-&mdN$ujZaG*7V3-b))a*vFpcfSI?j2-8%&Lj;sj|6FhCXU7ULh+*#9-(YEgvAemHb0?DK_PDxclvQV-L>(TZD54;~W z^V^4n?L)kC>#UVqzZLFB{~t61&M}q=gKdE10_}2O36EI<3(i2!|6cdqBl*Uy`Srnk z%N}rZ--T}OyU@*j3%a>)sYHi&H#)q7Gfn~g67=`l(h}|)A2f42kMga@gw|uc^SIzV z&N+|&&F4!FQ1~@4Go^6JTEV$G-a7;?wY_(HAqY2b?G&t?oV9bw>RUONd*(gruKE6j zKYQvFclv4msWZY;XSnTW;pUs45t^Ujtzp3$=F~St!UUn`57dc)CvhRl%P~0&>-0x^ zs(Ou@_g$WY+jZ}6vSN5Ub#SNY{heA2?OR-3HT-4T$q& zGl^kK*OTz_q0!v;gziIES504s?x!6(cu`3ds4+I+ONpFP)It9N$S);2=-P4$xXnXfl*5~~jo`FijYS(2mM+Oj zM&

)q}24v}r{$87P&_*4Bw>vn(;G+ksBAZ0O4!H-W}m3G=sdfe{x>^t2U@muyPg zLDwuhOoGn{ejxD>`@nQfL^3Z8cl zF1efiI#exE=+ekaB>zKrpLPvN#$7Py;)lqR1$SDrJ0LNWpjlE_ME&H{aB^x`G*1Fd zj(Rf>K`FRzRz$o-T;UV)2Ef_~d6ZlcEQT*(h1(8~|1SRzl zLgP=t{Nt8O2NSeptc8x8XOgTIR!Rf>lNtcP8PMbtSg-|nTfJbb2ZIk~UZgw=l!vS8 z;Hgf5>g1@-hn4=Dd#~?(ZU2q^@C||P2!2Tb5cr72HM`^5{<*OQOEYI_&bxrZ3$DgR z*X9M+=J`I})gib#7G1j+T)XdY=Uw{+*Zv0@!F4EmDDT^l)8BF|`q~zJ;JV2Bb_%|o z+5U&XE_xOmb)2Ja$=A5(YhLg*bHTm$wE*DeeftF8zFA$q5{X#F5pt=v?pAnS$Je$A zwQaM#OKv|`eRw_z0PcIE_sTn|CjDa1f`kbYLJJP~W&pt+(J%_%!o$bIED{t+vt)G`!l{7o!Q{j? zu)ay(z9(e`6bX>%EhLT?KYec-Nw7&4O*f(iz8+(v@hhp=NK*Q)R(6)rNe;a75huzT zqG9U!7~}$%nv3|UJd1KaiyFA7lV+X8lO9nMOR#7xm!%i9OR;#i3!cLl(DemQ8KkpP z`1Wmzj*4ddWSM55VAu#F=y?pEN{d?f>P<9Cn*~GgnJ@UMh>osf$4;E->5iNj?CZ^MF@%dQ_|SM zu7%}-zBf}7WuJ<`*W~hNCi~cpu+`uulAA#J1R;kc^TZzR*WGzyFSpj6C)zpnohNEI z^_?foGtw_lxVXZdCoD74FHhKKq+gzBa)moj zSZAbPp3q%2FA|;w!ZRD?37@eZgA>eblLVG61PRiTp45DX zU|9k#!rnS#&N~`sta-b8#*(k7ov}Z()k2)D0zk!v8GGJhoiRSNbzvL=hcA0&?l~Yz zyrWKV)XiA2Uh9nJswI0Q=jMowJh4$AHbMZ$XN_pUxB9bt=a0?r<1D**%WlE48#-q3 zW%uSO2OP@JPULOt@>K!&nkw(z$9eZH+YDAPn*jI>!HjX)O*lQ-sl2HYZ$LIUmQ6Y* z-~jjx0W{0rLVA{o)!-A1`%FnVOA=OJcF&xdw=@Wr1}L4XCc&+{2zG73*CO~jX(HhS zs|czHjrrx;7i+Uuc+xMB{(NxDynBA+j{h!Txk(l{1j4x^rEhZh6D^^shAhh(39l7P zs(G=7v)A)vgFrScYYgOJGIt2%VtAH9%-LAPSaub2Hu2;}f!w&PagfKzToS%&g=ZgMbHx1P1;CgYbCIrpsd;H1+Aac`X5s6nSOzC&mFtx=BOP!wL_qGEK++G zs69NjPoVa3)IOMEokzmrYPQ}7w0&~YBGtV>b@NoOK=pD|??cKl(|7ggtadIq-^fuR Vo(c(62m>#LWs8M1}{~KS~y@db( diff --git a/api_old/before/__pycache__/mediapipe.cpython-311.pyc b/api_old/before/__pycache__/mediapipe.cpython-311.pyc deleted file mode 100644 index 0e013dc20a35637c3402ddfa9d61ecd4f2e6f776..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17394 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 D;CMVQ diff --git a/api_old/before/cpm_api.py b/api_old/before/cpm_api.py deleted file mode 100644 index 172e1ce..0000000 --- a/api_old/before/cpm_api.py +++ /dev/null @@ -1,369 +0,0 @@ -import os -import json -import uuid -from datetime import datetime, timedelta -from fastapi import FastAPI, HTTPException, UploadFile, File -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from kafka import KafkaProducer, KafkaConsumer -from transformers import AutoTokenizer, AutoModel -from decord import VideoReader, cpu -from PIL import Image -from redis import Redis -import io -import re -import torch -import asyncio -from contextlib import asynccontextmanager -import threading - -app = FastAPI() -cpm_app = FastAPI() -app.mount("/cpm", cpm_app) - -# CORS设置 -ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/worker_sys/OpenBMB/MiniCPM-V-2_6" -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "cpm" -KAFKA_GROUP_ID = "cpm_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 5 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -# 设置 GPU 设备 -torch.cuda.set_device(0) - -# 初始化模型 -model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True) -model = model.half().cuda().eval() -tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) - -class MediaAnalysisSystem: - def __init__(self, model, tokenizer): - self.model = model - self.tokenizer = tokenizer - self.device = torch.device("cuda:0") - self.model = self.model.to(self.device) - self.MAX_NUM_FRAMES = 16 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - - def process_video(self, video_data, object_name): - if not video_data: - raise ValueError(f"Empty video data for {object_name}") - print(f"Processing video: {object_name}, data size: {len(video_data)} bytes") - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - answer = self.model.chat( - image=frames, # 直接传递 frames - msgs=msgs, - tokenizer=self.tokenizer, - max_length=512, - temperature=0.7, - top_p=0.9, - **params - ) - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - # "start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"), - # "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S") - } - - def process_image(self, image_data, object_name): - image = Image.open(io.BytesIO(image_data)) - question = "描述这张图片,包括场景、人物数量和行为等细节。" - msgs = [ - {'role': 'user', 'content': [image] + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - max_length=512, - temperature=0.7, - top_p=0.9, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - @staticmethod - def extract_time_from_filename(object_name): - filename = os.path.basename(object_name) - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 初始化 MediaAnalysisSystem -media_analysis_system = MediaAnalysisSystem(model, tokenizer) - -async def process_file(file: UploadFile, file_type: str): - content = await file.read() - # 获取原始文件的后缀 - original_extension = os.path.splitext(file.filename)[1] - - # 生成新的文件名,包含 UUID 和原始后缀 - filename = f"cpm_{uuid.uuid4()}{original_extension}" - file_path = os.path.join(UPLOAD_DIR, filename) - with open(file_path, "wb") as f: - f.write(content) - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": filename, - "type": file_type - }).encode('utf-8')) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return {"message": f"{file_type.capitalize()} uploaded and queued for processing", "filename": filename} - -@cpm_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - try: - file_type = "image" if file.content_type.startswith("image") else "video" - return await process_file(file, file_type) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@cpm_app.post("/analyze_video") -async def analyze_video(file: UploadFile = File(...)): - try: - return await process_file(file, "video") - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@cpm_app.post("/analyze_image") -async def analyze_image(file: UploadFile = File(...)): - try: - return await process_file(file, "image") - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -def process_task(): - for message in consumer: - try: - if isinstance(message.value, dict): - task = message.value - else: - task = json.loads(message.value.decode('utf-8')) - - filename = task['filename'] - file_type = task['type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - with open(file_path, 'rb') as f: - file_data = f.read() - - if file_type == "video": - result = media_analysis_system.process_video(file_data, filename) - elif file_type == "image": - result = media_analysis_system.process_image(file_data, filename) - - # 保存结果到 JSON 文件 - result_file_path = os.path.join(RESULT_DIR, f"{filename}.json") - with open(result_file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - # 将结果存储在 Redis 中 - redis_client.set(redis_key, json.dumps({ - "status": "completed", - "result": result - })) - - except Exception as e: - print(f"Error processing task: {str(e)}") - if 'filename' in locals() and 'file_type' in locals(): - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - else: - print("Error occurred before task details were extracted") - -@cpm_app.get("/result/{filename}") -async def get_result(filename: str): - for file_type in ["video", "image"]: - redis_key = f"{file_type}_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_json = json.loads(result) - - if result_json.get("status") == "queued": - return {"status": "queued", "message": "Your request is in the queue and will be processed soon."} - elif result_json.get("status") == "processing": - return {"status": "processing", "message": "Your request is being processed."} - else: - return result_json - - raise HTTPException(status_code=404, detail="Result not found") - -async def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@5__:*_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - print(f"Key changed: {key}") - -if __name__ == "__main__": - # 在后台线程中启动Kafka消费者 - consumer_thread = threading.Thread(target=process_task, daemon=True) - consumer_thread.start() - - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7000) \ No newline at end of file diff --git a/api_old/before/cpm_key.py b/api_old/before/cpm_key.py deleted file mode 100644 index 8530610..0000000 --- a/api_old/before/cpm_key.py +++ /dev/null @@ -1,518 +0,0 @@ -import os -import json -import uuid -from datetime import datetime, timedelta, timezone -from fastapi import FastAPI, HTTPException, UploadFile, File, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.security import APIKeyHeader -from kafka import KafkaProducer, KafkaConsumer -from transformers import AutoTokenizer, AutoModel -from decord import VideoReader, cpu -from PIL import Image -from redis import Redis -import io -import re -import torch -import asyncio -from contextlib import asynccontextmanager -import threading - -app = FastAPI() -cpm_app = FastAPI() -app.mount("/cpm", cpm_app) - -# CORS设置 -ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/worker_sys/OpenBMB/MiniCPM-V-2_6" -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "cpm" -KAFKA_GROUP_ID = "cpm_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 5 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 - - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) - -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = Image.open(file_path) - width, height = img.size - pixel_count = width * height - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - vr = VideoReader(file_path) - fps = vr.get_avg_fps() - frame_count = len(vr) - width, height = vr[0].shape[1], vr[0].shape[0] - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - -# 设置 GPU 设备 -torch.cuda.set_device(0) - -# 初始化模型 -model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True) -model = model.half().cuda().eval() -tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) - -class MediaAnalysisSystem: - def __init__(self, model, tokenizer): - self.model = model - self.tokenizer = tokenizer - self.device = torch.device("cuda:0") - self.model = self.model.to(self.device) - self.MAX_NUM_FRAMES = 16 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - - def process_video(self, video_data, object_name): - if not video_data: - raise ValueError(f"Empty video data for {object_name}") - print(f"Processing video: {object_name}, data size: {len(video_data)} bytes") - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - answer = self.model.chat( - image=frames, # 直接传递 frames - msgs=msgs, - tokenizer=self.tokenizer, - max_length=512, - temperature=0.7, - top_p=0.9, - **params - ) - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - # "start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"), - # "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S") - } - - def process_image(self, image_data, object_name): - image = Image.open(io.BytesIO(image_data)) - question = "描述这张图片,包括场景、人物数量和行为等细节。" - msgs = [ - {'role': 'user', 'content': [image] + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - max_length=512, - temperature=0.7, - top_p=0.9, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - @staticmethod - def extract_time_from_filename(object_name): - filename = os.path.basename(object_name) - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 初始化 MediaAnalysisSystem -media_analysis_system = MediaAnalysisSystem(model, tokenizer) - -async def process_file(file: UploadFile, file_type: str, api_key: str): - content = await file.read() - original_extension = os.path.splitext(file.filename)[1] - - filename = f"cpm_{uuid.uuid4()}{original_extension}" - file_path = os.path.join(UPLOAD_DIR, filename) - with open(file_path, "wb") as f: - f.write(content) - - # 计算token - tokens_required = calculate_tokens(file_path, file_type) - - # 检查并更新token使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新token使用量 - model_name = "MiniCPM-V-2_6" - await update_token_usage(api_key, tokens_required, model_name) - - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": filename, - "type": file_type - }).encode('utf-8')) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - -@cpm_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - file_type = "image" if file.content_type.startswith("image") else "video" - return await process_file(file, file_type, api_key) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - - -@cpm_app.post("/analyze_video") -async def analyze_video(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "video", api_key) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@cpm_app.post("/analyze_image") -async def analyze_image(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "image", api_key) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - - -def process_task(): - for message in consumer: - try: - if isinstance(message.value, dict): - task = message.value - else: - task = json.loads(message.value.decode('utf-8')) - - filename = task['filename'] - file_type = task['type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - with open(file_path, 'rb') as f: - file_data = f.read() - - if file_type == "video": - result = media_analysis_system.process_video(file_data, filename) - elif file_type == "image": - result = media_analysis_system.process_image(file_data, filename) - - # 保存结果到 JSON 文件 - result_file_path = os.path.join(RESULT_DIR, f"{filename}.json") - with open(result_file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - # 将结果存储在 Redis 中 - redis_client.set(redis_key, json.dumps({ - "status": "completed", - "result": result - })) - - except Exception as e: - print(f"Error processing task: {str(e)}") - if 'filename' in locals() and 'file_type' in locals(): - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - else: - print("Error occurred before task details were extracted") - -@cpm_app.get("/result/{filename}") -async def get_result(filename: str): - for file_type in ["video", "image"]: - redis_key = f"{file_type}_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_json = json.loads(result) - - if result_json.get("status") == "queued": - return {"status": "queued", "message": "Your request is in the queue and will be processed soon."} - elif result_json.get("status") == "processing": - return {"status": "processing", "message": "Your request is being processed."} - else: - return result_json - - raise HTTPException(status_code=404, detail="Result not found") - -async def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@5__:*_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - print(f"Key changed: {key}") - -if __name__ == "__main__": - # 在后台线程中启动Kafka消费者 - consumer_thread = threading.Thread(target=process_task, daemon=True) - consumer_thread.start() - - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7000) \ No newline at end of file diff --git a/api_old/before/cpm_key2.py b/api_old/before/cpm_key2.py deleted file mode 100644 index 8f95815..0000000 --- a/api_old/before/cpm_key2.py +++ /dev/null @@ -1,526 +0,0 @@ -import os -import json -import uuid -from datetime import datetime, timedelta, timezone -from fastapi import FastAPI, HTTPException, UploadFile, File, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.security import APIKeyHeader -from kafka import KafkaProducer, KafkaConsumer -from transformers import AutoTokenizer, AutoModel -from decord import VideoReader, cpu -from PIL import Image -from redis import Redis -import io -import re -import torch -from contextlib import asynccontextmanager -import threading -import string - - -app = FastAPI() -cpm_app = FastAPI() -app.mount("/cpm", cpm_app) - -# CORS设置 -ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/worker_sys/OpenBMB/MiniCPM-V-2_6" -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "cpm" -KAFKA_GROUP_ID = "cpm_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 5 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 - - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) - -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = Image.open(file_path) - width, height = img.size - pixel_count = width * height - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - vr = VideoReader(file_path) - fps = vr.get_avg_fps() - frame_count = len(vr) - width, height = vr[0].shape[1], vr[0].shape[0] - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - -# 设置 GPU 设备 -torch.cuda.set_device(0) - -# 初始化模型 -model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True) -model = model.half().cuda().eval() -tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) - -class MediaAnalysisSystem: - def __init__(self, model, tokenizer): - self.model = model - self.tokenizer = tokenizer - self.device = torch.device("cuda:0") - self.model = self.model.to(self.device) - self.MAX_NUM_FRAMES = 16 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file, ctx=cpu(0)) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - - def process_video(self, video_data, object_name): - if not video_data: - raise ValueError(f"Empty video data for {object_name}") - print(f"Processing video: {object_name}, data size: {len(video_data)} bytes") - frames = self.encode_video(video_data) - question = "Describe the video in as much detail as possible in Chinese, including the setting, clear number of people, and changes in behavior." - msgs = [ - {'role': 'user', 'content': frames + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - answer = self.model.chat( - image=frames, # 直接传递 frames - msgs=msgs, - tokenizer=self.tokenizer, - max_length=512, - temperature=0.7, - top_p=0.9, - **params - ) - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "num_frames": len(frames), - # "start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"), - # "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S") - } - - def process_image(self, image_data, object_name): - image = Image.open(io.BytesIO(image_data)) - question = "描述这张图片,包括场景、人物数量和行为等细节。" - msgs = [ - {'role': 'user', 'content': [image] + [question]}, - ] - - params = { - "use_image_id": False, - "max_slice_nums": 1 - } - - answer = self.model.chat( - image=None, - msgs=msgs, - tokenizer=self.tokenizer, - max_length=512, - temperature=0.7, - top_p=0.9, - **params - ) - - extracted_info = self.extract_info(answer) - - return { - "original_answer": answer, - "extracted_info": extracted_info, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - @staticmethod - def extract_time_from_filename(object_name): - filename = os.path.basename(object_name) - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - for action in actions: - if action in answer: - info["actions"].append(action) - - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - for interaction in interactions: - if interaction in answer: - info["interactions"].append(interaction) - - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for obj in objects: - if obj in answer: - info["objects"].append(obj) - - for item in furniture: - if item in answer: - info["furniture"].append(item) - - return info - -# 初始化 MediaAnalysisSystem -media_analysis_system = MediaAnalysisSystem(model, tokenizer) - -async def process_file(file: UploadFile, file_type: str, api_key_info: dict): - content = await file.read() - original_extension = os.path.splitext(file.filename)[1] - - filename = f"cpm_{uuid.uuid4()}{original_extension}" - file_path = os.path.join(UPLOAD_DIR, filename) - with open(file_path, "wb") as f: - f.write(content) - - # 计算token - tokens_required = calculate_tokens(file_path, file_type) - - # 检查并更新token使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新token使用量 - model_name = "MiniCPM-V-2_6" - await update_token_usage(api_key, tokens_required, model_name) - - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": filename, - "type": file_type - }).encode('utf-8')) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - -@cpm_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - try: - file_type = "image" if file.content_type.startswith("image") else "video" - return await process_file(file, file_type, api_key_info) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - - -@cpm_app.post("/analyze_video") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - api_key_info = await verify_api_key(api_key_info) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "video", api_key_info) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@cpm_app.post("/analyze_image") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - api_key_info = await verify_api_key(api_key_info) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "image", api_key_info) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - - -def process_task(): - for message in consumer: - try: - if isinstance(message.value, dict): - task = message.value - else: - task = json.loads(message.value.decode('utf-8')) - - filename = task['filename'] - file_type = task['type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - with open(file_path, 'rb') as f: - file_data = f.read() - - if file_type == "video": - result = media_analysis_system.process_video(file_data, filename) - elif file_type == "image": - result = media_analysis_system.process_image(file_data, filename) - - # 保存结果到 JSON 文件 - result_file_path = os.path.join(RESULT_DIR, f"{filename}.json") - with open(result_file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - # 将结果存储在 Redis 中 - redis_client.set(redis_key, json.dumps({ - "status": "completed", - "result": result - })) - - except Exception as e: - print(f"Error processing task: {str(e)}") - if 'filename' in locals() and 'file_type' in locals(): - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - else: - print("Error occurred before task details were extracted") - -@cpm_app.get("/result/{filename}") -async def get_result(filename: str): - for file_type in ["video", "image"]: - redis_key = f"{file_type}_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_json = json.loads(result) - - if result_json.get("status") == "queued": - return {"status": "queued", "message": "Your request is in the queue and will be processed soon."} - elif result_json.get("status") == "processing": - return {"status": "processing", "message": "Your request is being processed."} - else: - return result_json - - raise HTTPException(status_code=404, detail="Result not found") - -async def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@5__:*_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - print(f"Key changed: {key}") - -if __name__ == "__main__": - # 在后台线程中启动Kafka消费者 - consumer_thread = threading.Thread(target=process_task, daemon=True) - consumer_thread.start() - - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7000) \ No newline at end of file diff --git a/api_old/before/face_api.py b/api_old/before/face_api.py deleted file mode 100644 index 118babe..0000000 --- a/api_old/before/face_api.py +++ /dev/null @@ -1,312 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.middleware.cors import CORSMiddleware -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import io -import uuid -import os -from datetime import datetime, timedelta -import threading -import torch -torch.cuda.set_device(1) - - -app = FastAPI() -face_app = FastAPI() -app.mount("/face", face_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8n-face.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "face" # 指定Kafka topic -KAFKA_GROUP_ID = "face_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 7 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -class faceDetector: - def __init__(self, model_path): - self.model = YOLO(model_path).to('cuda:1') - - def detect(self, frame): - results = self.model(frame, device='cuda:1') - return results - - def format_results(self, results): - formatted_results = [] - for r in results: - boxes = r.boxes - keypoints = r.keypoints - for i in range(len(boxes)): - box = boxes[i] - kpts = keypoints[i] - formatted_results.append({ - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "keypoints": kpts.xy.tolist()[0] - }) - return formatted_results - - def draw_results(self, frame, results, original_shape): - for r in results: - annotated_frame = r.plot(img=frame) - # 调整坐标以适应原始图像大小 - h, w = annotated_frame.shape[:2] - scale_x, scale_y = original_shape[1] / w, original_shape[0] / h - annotated_frame = cv2.resize(annotated_frame, (original_shape[1], original_shape[0])) - return annotated_frame - -detector = faceDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = img.shape - # Convert BGR to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Resize image to fit model requirements (640x640) - img_resized = cv2.resize(img, (640, 640)) - - # Normalize and reshape to BCHW format - img_tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - img_tensor = img_tensor.to('cuda:1') - - results = detector.detect(img_tensor) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on original image - annotated_img = detector.draw_results(img_resized, results, original_shape) - - # Save annotated image - annotated_filename = f"face_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, cv2.cvtColor(annotated_img, cv2.COLOR_RGB2BGR)) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"face_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - # Create output video file - annotated_filename = f"face_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process one frame per second - if frame_count % fps == 0: - # Convert BGR to RGB - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Resize frame to fit model requirements (640x640) - frame_resized = cv2.resize(frame_rgb, (640, 640)) - - # Normalize and reshape to BCHW format - frame_tensor = torch.from_numpy(frame_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - frame_tensor = frame_tensor.to('cuda:1') - - results = detector.detect(frame_tensor) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - # Draw results on original frame - annotated_frame = detector.draw_results(frame_resized, results, original_shape) - # Convert RGB back to BGR for OpenCV - annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@face_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"face_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return JSONResponse(content={"message": "File uploaded and queued for processing", "filename": new_filename}) - -@face_app.get("/result/{filename}") -async def get_face_result(filename: str): - redis_key = f"face_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@face_app.get("/annotated/{filename}") -async def get_annotated_file(filename: str): - redis_key = f"face_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"face_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:face_result:*') # 监听所有face_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"face_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7004) \ No newline at end of file diff --git a/api_old/before/face_key.py b/api_old/before/face_key.py deleted file mode 100644 index 5ad8e29..0000000 --- a/api_old/before/face_key.py +++ /dev/null @@ -1,462 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import torch -torch.cuda.set_device(1) - - -app = FastAPI() -face_app = FastAPI() -app.mount("/face", face_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8n-face.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "face" # 指定Kafka topic -KAFKA_GROUP_ID = "face_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 7 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class faceDetector: - def __init__(self, model_path): - self.model = YOLO(model_path).to('cuda:1') - - def detect(self, frame): - results = self.model(frame, device='cuda:1') - return results - - def format_results(self, results): - formatted_results = [] - for r in results: - boxes = r.boxes - keypoints = r.keypoints - for i in range(len(boxes)): - box = boxes[i] - kpts = keypoints[i] - formatted_results.append({ - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "keypoints": kpts.xy.tolist()[0] - }) - return formatted_results - - def draw_results(self, frame, results, original_shape): - for r in results: - annotated_frame = r.plot(img=frame) - # 调整坐标以适应原始图像大小 - h, w = annotated_frame.shape[:2] - scale_x, scale_y = original_shape[1] / w, original_shape[0] / h - annotated_frame = cv2.resize(annotated_frame, (original_shape[1], original_shape[0])) - return annotated_frame - -detector = faceDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = img.shape - # Convert BGR to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Resize image to fit model requirements (640x640) - img_resized = cv2.resize(img, (640, 640)) - - # Normalize and reshape to BCHW format - img_tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - img_tensor = img_tensor.to('cuda:1') - - results = detector.detect(img_tensor) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on original image - annotated_img = detector.draw_results(img_resized, results, original_shape) - - # Save annotated image - annotated_filename = f"face_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, cv2.cvtColor(annotated_img, cv2.COLOR_RGB2BGR)) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"face_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - # Create output video file - annotated_filename = f"face_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process one frame per second - if frame_count % fps == 0: - # Convert BGR to RGB - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Resize frame to fit model requirements (640x640) - frame_resized = cv2.resize(frame_rgb, (640, 640)) - - # Normalize and reshape to BCHW format - frame_tensor = torch.from_numpy(frame_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - frame_tensor = frame_tensor.to('cuda:1') - - results = detector.detect(frame_tensor) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - # Draw results on original frame - annotated_frame = detector.draw_results(frame_resized, results, original_shape) - # Convert RGB back to BGR for OpenCV - annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@face_app.post("/upload") -async def upload_file(file: UploadFile = File(...),api_key: str = Depends(get_api_key)): - # 验证 API key - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - - # 检查并更新 token 使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8n-face" - await update_token_usage(api_key, tokens_required, model_name) - - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"face_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - -@face_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_face_result(filename: str): - redis_key = f"face_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@face_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"face_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"face_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:face_result:*') # 监听所有face_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"face_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7004) \ No newline at end of file diff --git a/api_old/before/face_key2.py b/api_old/before/face_key2.py deleted file mode 100644 index 02e40e4..0000000 --- a/api_old/before/face_key2.py +++ /dev/null @@ -1,471 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import torch - -import string -torch.cuda.set_device(1) - - -app = FastAPI() -face_app = FastAPI() -app.mount("/face", face_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8n-face.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "face" # 指定Kafka topic -KAFKA_GROUP_ID = "face_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 7 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class faceDetector: - def __init__(self, model_path): - self.model = YOLO(model_path).to('cuda:1') - - def detect(self, frame): - results = self.model(frame, device='cuda:1') - return results - - def format_results(self, results): - formatted_results = [] - for r in results: - boxes = r.boxes - keypoints = r.keypoints - for i in range(len(boxes)): - box = boxes[i] - kpts = keypoints[i] - formatted_results.append({ - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "keypoints": kpts.xy.tolist()[0] - }) - return formatted_results - - def draw_results(self, frame, results, original_shape): - for r in results: - annotated_frame = r.plot(img=frame) - # 调整坐标以适应原始图像大小 - h, w = annotated_frame.shape[:2] - scale_x, scale_y = original_shape[1] / w, original_shape[0] / h - annotated_frame = cv2.resize(annotated_frame, (original_shape[1], original_shape[0])) - return annotated_frame - -detector = faceDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = img.shape - # Convert BGR to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Resize image to fit model requirements (640x640) - img_resized = cv2.resize(img, (640, 640)) - - # Normalize and reshape to BCHW format - img_tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - img_tensor = img_tensor.to('cuda:1') - - results = detector.detect(img_tensor) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on original image - annotated_img = detector.draw_results(img_resized, results, original_shape) - - # Save annotated image - annotated_filename = f"face_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, cv2.cvtColor(annotated_img, cv2.COLOR_RGB2BGR)) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"face_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - # Create output video file - annotated_filename = f"face_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process one frame per second - if frame_count % fps == 0: - # Convert BGR to RGB - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Resize frame to fit model requirements (640x640) - frame_resized = cv2.resize(frame_rgb, (640, 640)) - - # Normalize and reshape to BCHW format - frame_tensor = torch.from_numpy(frame_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - frame_tensor = frame_tensor.to('cuda:1') - - results = detector.detect(frame_tensor) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - # Draw results on original frame - annotated_frame = detector.draw_results(frame_resized, results, original_shape) - # Convert RGB back to BGR for OpenCV - annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@face_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8n-face" - await update_token_usage(api_key, tokens_required, model_name) - - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"face_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - -@face_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_face_result(filename: str): - redis_key = f"face_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@face_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"face_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"face_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:face_result:*') # 监听所有face_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"face_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7004) \ No newline at end of file diff --git a/api_old/before/fall_api.py b/api_old/before/fall_api.py deleted file mode 100644 index d85705a..0000000 --- a/api_old/before/fall_api.py +++ /dev/null @@ -1,293 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.middleware.cors import CORSMiddleware -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import io -import uuid -import os -from datetime import datetime, timedelta -import threading - -app = FastAPI() -fall_app = FastAPI() -app.mount("/fall", fall_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8n-fall.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "fall" # 指定Kafka topic -KAFKA_GROUP_ID = "fall_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 4 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -class fallDetector: - def __init__(self, model_path): - self.model = YOLO(model_path) - def detect(self, frame): - results = self.model(frame) - return results - def format_results(self, results): - formatted_results = [] - for r in results: - if not hasattr(r, 'boxes') or len(r.boxes) == 0: - print("没有检测到任何对象") - return [{"message": "No objects detected"}] - - boxes = r.boxes - names = getattr(r, 'names', {}) - - for i in range(len(boxes)): - box = boxes[i] - if not hasattr(box, 'cls') or not hasattr(box, 'conf') or not hasattr(box, 'xyxy'): - print(f"警告: 第 {i} 个框缺少必要的属性") - continue - - try: - class_id = int(box.cls.item()) - formatted_result = { - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "class_id": class_id, - "class": names.get(class_id, f"Unknown-{class_id}") - } - formatted_results.append(formatted_result) - except Exception as e: - print(f"处理第 {i} 个框时出错: {str(e)}") - - # print("格式化后的结果:", formatted_results) - return formatted_results - - def draw_results(self, frame, results): - for r in results: - annotated_frame = r.plot() - return annotated_frame - -detector = fallDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - - results = detector.detect(img) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on image - annotated_img = detector.draw_results(img, results) - - # Save annotated image - annotated_filename = f"fall_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - - - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"fall_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - # Create output video file - annotated_filename = f"fall_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - results = detector.detect(frame) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - annotated_frame = detector.draw_results(frame, results) - out.write(annotated_frame) - - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@fall_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"fall_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return JSONResponse(content={"message": "File uploaded and queued for processing", "filename": new_filename}) - -@fall_app.get("/result/{filename}") -async def get_fall_result(filename: str): - redis_key = f"fall_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@fall_app.get("/annotated/{filename}") -async def get_annotated_file(filename: str): - redis_key = f"fall_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"fall_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:fall_result:*') # 监听所有fall_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"fall_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7002) \ No newline at end of file diff --git a/api_old/before/fall_key.py b/api_old/before/fall_key.py deleted file mode 100644 index 3894b2b..0000000 --- a/api_old/before/fall_key.py +++ /dev/null @@ -1,442 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import io -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading - -app = FastAPI() -fall_app = FastAPI() -app.mount("/fall", fall_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8n-fall.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "fall" # 指定Kafka topic -KAFKA_GROUP_ID = "fall_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 4 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class fallDetector: - def __init__(self, model_path): - self.model = YOLO(model_path) - def detect(self, frame): - results = self.model(frame) - return results - def format_results(self, results): - formatted_results = [] - for r in results: - if not hasattr(r, 'boxes') or len(r.boxes) == 0: - print("没有检测到任何对象") - return [{"message": "No objects detected"}] - - boxes = r.boxes - names = getattr(r, 'names', {}) - - for i in range(len(boxes)): - box = boxes[i] - if not hasattr(box, 'cls') or not hasattr(box, 'conf') or not hasattr(box, 'xyxy'): - print(f"警告: 第 {i} 个框缺少必要的属性") - continue - - try: - class_id = int(box.cls.item()) - formatted_result = { - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "class_id": class_id, - "class": names.get(class_id, f"Unknown-{class_id}") - } - formatted_results.append(formatted_result) - except Exception as e: - print(f"处理第 {i} 个框时出错: {str(e)}") - - # print("格式化后的结果:", formatted_results) - return formatted_results - - def draw_results(self, frame, results): - for r in results: - annotated_frame = r.plot() - return annotated_frame - -detector = fallDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - - results = detector.detect(img) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on image - annotated_img = detector.draw_results(img, results) - - # Save annotated image - annotated_filename = f"fall_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - - - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"fall_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - # Create output video file - annotated_filename = f"fall_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - results = detector.detect(frame) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - annotated_frame = detector.draw_results(frame, results) - out.write(annotated_frame) - - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@fall_app.post("/upload") -async def upload_file(file: UploadFile = File(...),api_key: str = Depends(get_api_key)): - # 验证 API key - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - # 检查并更新 token 使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8n-fall" - await update_token_usage(api_key, tokens_required, model_name) - - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"fall_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@fall_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_fall_result(filename: str): - redis_key = f"fall_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@fall_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"fall_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"fall_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:fall_result:*') # 监听所有fall_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"fall_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7002) \ No newline at end of file diff --git a/api_old/before/fall_key2.py b/api_old/before/fall_key2.py deleted file mode 100644 index 82abe42..0000000 --- a/api_old/before/fall_key2.py +++ /dev/null @@ -1,449 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import io -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import string -app = FastAPI() -fall_app = FastAPI() -app.mount("/fall", fall_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8n-fall.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "fall" # 指定Kafka topic -KAFKA_GROUP_ID = "fall_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 4 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class fallDetector: - def __init__(self, model_path): - self.model = YOLO(model_path) - def detect(self, frame): - results = self.model(frame) - return results - def format_results(self, results): - formatted_results = [] - for r in results: - if not hasattr(r, 'boxes') or len(r.boxes) == 0: - print("没有检测到任何对象") - return [{"message": "No objects detected"}] - - boxes = r.boxes - names = getattr(r, 'names', {}) - - for i in range(len(boxes)): - box = boxes[i] - if not hasattr(box, 'cls') or not hasattr(box, 'conf') or not hasattr(box, 'xyxy'): - print(f"警告: 第 {i} 个框缺少必要的属性") - continue - - try: - class_id = int(box.cls.item()) - formatted_result = { - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "class_id": class_id, - "class": names.get(class_id, f"Unknown-{class_id}") - } - formatted_results.append(formatted_result) - except Exception as e: - print(f"处理第 {i} 个框时出错: {str(e)}") - - # print("格式化后的结果:", formatted_results) - return formatted_results - - def draw_results(self, frame, results): - for r in results: - annotated_frame = r.plot() - return annotated_frame - -detector = fallDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - - results = detector.detect(img) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on image - annotated_img = detector.draw_results(img, results) - - # Save annotated image - annotated_filename = f"fall_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - - - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"fall_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - # Create output video file - annotated_filename = f"fall_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - results = detector.detect(frame) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - annotated_frame = detector.draw_results(frame, results) - out.write(annotated_frame) - - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@fall_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8n-fall" - await update_token_usage(api_key, tokens_required, model_name) - - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"fall_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@fall_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_fall_result(filename: str): - redis_key = f"fall_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@fall_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"fall_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"fall_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:fall_result:*') # 监听所有fall_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"fall_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7002) \ No newline at end of file diff --git a/api_old/before/mediapipe.py b/api_old/before/mediapipe.py deleted file mode 100644 index 9d84690..0000000 --- a/api_old/before/mediapipe.py +++ /dev/null @@ -1,297 +0,0 @@ -import os -import cv2 -import torch -import numpy as np -from redis import Redis -import json -from kafka import KafkaConsumer -import threading -import redis -import torch -import media as mp -from mediapipe.tasks import python -from mediapipe.tasks.python import vision - -# Configuration -MODEL_PATH = "/home/zydi/models/face_landmarker.task" # Replace with your model path -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "mediapipe" -KAFKA_GROUP_ID = "mediapipe_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 9 # POSE Worker使用的Redis DB -MAIN_REDIS_DB = 15 # 主Redis DB -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" - -# Ensure directories exist -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# Initialize Kafka -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -main_redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=MAIN_REDIS_DB -) - - -class mediapipeEmbedder: - def __init__(self, model_path): - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions( - base_options=base_options, - output_face_blendshapes=True, - output_facial_transformation_matrixes=True, - num_faces=1 - ) - self.detector = vision.FaceLandmarker.create_from_options(options) - - def get_mediapipe_landmarks(self, image): - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image) - detection_result = self.detector.detect(mp_image) - if detection_result.face_landmarks: - return np.array([(lm.x, lm.y, lm.z) for lm in detection_result.face_landmarks[0]]) - return None - - def process_image(self, image_data): - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - landmarks = self.get_mediapipe_landmarks(img) - - if landmarks is not None: - # Calculate a more detailed mediapipe embedding - embedding = self.calculate_detailed_embedding(landmarks) - - # Draw landmarks on the image - for lm in landmarks: - cv2.circle(img, (int(lm[0]*img.shape[1]), int(lm[1]*img.shape[0])), 2, (0,255,0), -1) - - return { - "embedding": embedding, - "landmarks": landmarks.tolist() - }, img - else: - return None, img - - def calculate_detailed_embedding(self, landmarks): - # Calculate various statistical features - mean = np.mean(landmarks, axis=0) - std = np.std(landmarks, axis=0) - median = np.median(landmarks, axis=0) - min_vals = np.min(landmarks, axis=0) - max_vals = np.max(landmarks, axis=0) - - # Calculate pairwise distances between key facial landmarks - nose_tip = landmarks[4] - left_eye = landmarks[159] - right_eye = landmarks[386] - left_mouth = landmarks[61] - right_mouth = landmarks[291] - - eye_distance = np.linalg.norm(left_eye - right_eye) - mouth_width = np.linalg.norm(left_mouth - right_mouth) - nose_to_mouth = np.linalg.norm(nose_tip - (left_mouth + right_mouth) / 2) - - # Calculate face shape features - face_width = np.max(landmarks[:, 0]) - np.min(landmarks[:, 0]) - face_height = np.max(landmarks[:, 1]) - np.min(landmarks[:, 1]) - face_depth = np.max(landmarks[:, 2]) - np.min(landmarks[:, 2]) - - # Combine all features into a single embedding - embedding = np.concatenate([ - mean, std, median, min_vals, max_vals, - [eye_distance, mouth_width, nose_to_mouth, face_width, face_height, face_depth] - ]) - - return embedding.tolist() - -embedder = mediapipeEmbedder(MODEL_PATH) - -def process_image(image_data, filename): - try: - results, annotated_img = embedder.process_image(image_data) - - if results: - # Save annotated image - annotated_filename = f"mediapipe_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return results, annotated_filename - else: - print(f"No face landmarks detected in image: {filename}") - return None, None - except Exception as e: - print(f"Error processing image {filename}: {str(e)}") - import traceback - traceback.print_exc() - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"mediapipe_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - annotated_filename = f"mediapipe_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - if frame_count % fps == 0: - frame_results, annotated_frame = embedder.process_image(cv2.imencode('.jpg', frame)[1].tobytes()) - if frame_results: - results.append({"frame": frame_count, "results": frame_results}) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -def process_task(): - print("开始处理任务,等待Kafka消息...") - for message in consumer: - print(f"收到Kafka消息: topic={message.topic}, partition={message.partition}, offset={message.offset}") - task = message.value - task_id = task['task_id'] - filename = task['filename'] - file_type = task['file_type'] - - print(f"解析任务信息: ID={task_id}, 文件名={filename}, 类型={file_type}") - - file_path = os.path.join(UPLOAD_DIR, filename) - # 检查键类型并更新状态 - task_key = f"task:{task_id}" - try: - key_type = main_redis_client.type(task_key) - if key_type != b'hash': - main_redis_client.delete(task_key) - main_redis_client.hset(f"task:{task_id}", "status", "processing") - print(f"任务 {task_id} 状态更新为 'processing'") - except redis.exceptions.ResponseError as e: - print(f"更新任务 {task_id} 状态时出错: {str(e)}") - continue # 跳过这个任务,继续处理下一个 - - try: - if file_type == "image": - print(f"开始处理图像: {filename}") - with open(file_path, 'rb') as f: - image_data = f.read() - json_results, annotated_filename = process_image(image_data, filename) - if json_results and annotated_filename is not None: - result_path = os.path.join(RESULT_DIR, annotated_filename) - - redis_client.hset(f"fall_result:{task_id}", mapping={ - "result": json.dumps(json_results), - "result_file": annotated_filename - }) - main_redis_client.hset(f"task:{task_id}", "status", "completed") - main_redis_client.hset(f"task:{task_id}", "result_type", "fall") - main_redis_client.hset(f"task:{task_id}", "result_key", f"fall_result:{task_id}") - print(f"图像 {filename} 处理完成,结果已保存") - else: - print(f"图像 {filename} 处理失败") - main_redis_client.hset(f"task:{task_id}", "status", "failed") - else: # video - print(f"开始处理视频: {filename}") - with open(file_path, 'rb') as f: - video_data = f.read() - json_results, annotated_filename = process_video(video_data, filename) - if json_results and annotated_filename: - redis_client.hset(f"fall_result:{task_id}", mapping={ - "result": json.dumps(json_results), - "result_file": annotated_filename - }) - main_redis_client.hset(f"task:{task_id}", "status", "completed") - main_redis_client.hset(f"task:{task_id}", "result_type", "fall") - main_redis_client.hset(f"task:{task_id}", "result_key", f"fall_result:{task_id}") - - - - print(f"视频 {filename} 处理完成,结果已保存") - else: - print(f"视频 {filename} 处理失败") - main_redis_client.hset(f"task:{task_id}", "status", "failed") - except Exception as e: - print(f"处理任务 {task_id} 时出错: {str(e)}") - main_redis_client.hset(f"task:{task_id}", { - "status": "failed", - "error": str(e) - }) - - print(f"任务 {task_id} 处理完毕,等待下一个Kafka消息...") - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:fall_result:*') # 监听所有fall_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'hset': - value = redis_client.hgetall(f"fall_result:{key}") - if value: - result = {k.decode(): v.decode() for k, v in value.items()} - print(f"Result update for task {key}: {result}") - - -if __name__ == "__main__": - print("fall处理程序启动...") - # 启动处理任务的线程 - task_thread = threading.Thread(target=process_task, daemon=True) - task_thread.start() - print("任务处理线程已启动") - - # 启动Redis监听线程 - redis_thread = threading.Thread(target=listen_redis_changes, daemon=True) - redis_thread.start() - print("Redis监听线程已启动") - - print("主程序进入等待状态...") - # 保持主线程运行 - task_thread.join() - redis_thread.join() \ No newline at end of file diff --git a/api_old/before/mediapipe_api.py b/api_old/before/mediapipe_api.py deleted file mode 100644 index 1b75b6c..0000000 --- a/api_old/before/mediapipe_api.py +++ /dev/null @@ -1,304 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.middleware.cors import CORSMiddleware -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import timedelta -import threading -import mediapipe as mp -from mediapipe.tasks import python -from mediapipe.tasks.python import vision - -app = FastAPI() -mediapipe_app = FastAPI() -app.mount("/mediapipe", mediapipe_app) - -# CORS configuration -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Configuration -MODEL_PATH = "/home/zydi/models/face_landmarker.task" # Replace with your model path -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "mediapipe" -KAFKA_GROUP_ID = "mediapipe_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 10 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# Ensure directories exist -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# Initialize Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# Initialize Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -class mediapipeEmbedder: - def __init__(self, model_path): - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions( - base_options=base_options, - output_face_blendshapes=True, - output_facial_transformation_matrixes=True, - num_faces=1 - ) - self.detector = vision.FaceLandmarker.create_from_options(options) - - def get_mediapipe_landmarks(self, image): - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image) - detection_result = self.detector.detect(mp_image) - if detection_result.face_landmarks: - return np.array([(lm.x, lm.y, lm.z) for lm in detection_result.face_landmarks[0]]) - return None - - def process_image(self, image_data): - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - landmarks = self.get_mediapipe_landmarks(img) - - if landmarks is not None: - # Calculate a more detailed mediapipe embedding - embedding = self.calculate_detailed_embedding(landmarks) - - # Draw landmarks on the image - for lm in landmarks: - cv2.circle(img, (int(lm[0]*img.shape[1]), int(lm[1]*img.shape[0])), 2, (0,255,0), -1) - - return { - "embedding": embedding, - "landmarks": landmarks.tolist() - }, img - else: - return None, img - - def calculate_detailed_embedding(self, landmarks): - # Calculate various statistical features - mean = np.mean(landmarks, axis=0) - std = np.std(landmarks, axis=0) - median = np.median(landmarks, axis=0) - min_vals = np.min(landmarks, axis=0) - max_vals = np.max(landmarks, axis=0) - - # Calculate pairwise distances between key facial landmarks - nose_tip = landmarks[4] - left_eye = landmarks[159] - right_eye = landmarks[386] - left_mouth = landmarks[61] - right_mouth = landmarks[291] - - eye_distance = np.linalg.norm(left_eye - right_eye) - mouth_width = np.linalg.norm(left_mouth - right_mouth) - nose_to_mouth = np.linalg.norm(nose_tip - (left_mouth + right_mouth) / 2) - - # Calculate face shape features - face_width = np.max(landmarks[:, 0]) - np.min(landmarks[:, 0]) - face_height = np.max(landmarks[:, 1]) - np.min(landmarks[:, 1]) - face_depth = np.max(landmarks[:, 2]) - np.min(landmarks[:, 2]) - - # Combine all features into a single embedding - embedding = np.concatenate([ - mean, std, median, min_vals, max_vals, - [eye_distance, mouth_width, nose_to_mouth, face_width, face_height, face_depth] - ]) - - return embedding.tolist() - -embedder = mediapipeEmbedder(MODEL_PATH) - -def process_image(image_data, filename): - try: - results, annotated_img = embedder.process_image(image_data) - - if results: - # Save annotated image - annotated_filename = f"mediapipe_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return results, annotated_filename - else: - print(f"No face landmarks detected in image: {filename}") - return None, None - except Exception as e: - print(f"Error processing image {filename}: {str(e)}") - import traceback - traceback.print_exc() - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"mediapipe_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - annotated_filename = f"mediapipe_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - if frame_count % fps == 0: - frame_results, annotated_frame = embedder.process_image(cv2.imencode('.jpg', frame)[1].tobytes()) - if frame_results: - results.append({"frame": frame_count, "results": frame_results}) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@mediapipe_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - redis_key = f"mediapipe_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return JSONResponse(content={"message": "File uploaded and queued for processing", "filename": new_filename}) - -@mediapipe_app.get("/result/{filename}") -async def get_mediapipe_result(filename: str): - redis_key = f"mediapipe_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) - else: - raise HTTPException(status_code=404, detail="Result not found") - -@mediapipe_app.get("/annotated/{filename}") -async def get_annotated_file(filename: str): - redis_key = f"mediapipe_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"mediapipe_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - results, annotated_filename = process_video(content, filename) - - if results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "results": results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:mediapipe_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"mediapipe_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - -if __name__ == "__main__": - threading.Thread(target=process_task, daemon=True).start() - threading.Thread(target=listen_redis_changes, daemon=True).start() - uvicorn.run(app, host="0.0.0.0", port=7006) \ No newline at end of file diff --git a/api_old/before/mediapipe_key.py b/api_old/before/mediapipe_key.py deleted file mode 100644 index 8b2c104..0000000 --- a/api_old/before/mediapipe_key.py +++ /dev/null @@ -1,453 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import mediapipe as mp -from mediapipe.tasks import python -from mediapipe.tasks.python import vision - -app = FastAPI() -mediapipe_app = FastAPI() -app.mount("/mediapipe", mediapipe_app) - -# CORS configuration -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Configuration -MODEL_PATH = "/home/zydi/models/face_landmarker.task" # Replace with your model path -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "mediapipe" -KAFKA_GROUP_ID = "mediapipe_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 10 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# Ensure directories exist -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# Initialize Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# Initialize Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class mediapipeEmbedder: - def __init__(self, model_path): - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions( - base_options=base_options, - output_face_blendshapes=True, - output_facial_transformation_matrixes=True, - num_faces=1 - ) - self.detector = vision.FaceLandmarker.create_from_options(options) - - def get_mediapipe_landmarks(self, image): - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image) - detection_result = self.detector.detect(mp_image) - if detection_result.face_landmarks: - return np.array([(lm.x, lm.y, lm.z) for lm in detection_result.face_landmarks[0]]) - return None - - def process_image(self, image_data): - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - landmarks = self.get_mediapipe_landmarks(img) - - if landmarks is not None: - # Calculate a more detailed mediapipe embedding - embedding = self.calculate_detailed_embedding(landmarks) - - # Draw landmarks on the image - for lm in landmarks: - cv2.circle(img, (int(lm[0]*img.shape[1]), int(lm[1]*img.shape[0])), 2, (0,255,0), -1) - - return { - "embedding": embedding, - "landmarks": landmarks.tolist() - }, img - else: - return None, img - - def calculate_detailed_embedding(self, landmarks): - # Calculate various statistical features - mean = np.mean(landmarks, axis=0) - std = np.std(landmarks, axis=0) - median = np.median(landmarks, axis=0) - min_vals = np.min(landmarks, axis=0) - max_vals = np.max(landmarks, axis=0) - - # Calculate pairwise distances between key facial landmarks - nose_tip = landmarks[4] - left_eye = landmarks[159] - right_eye = landmarks[386] - left_mouth = landmarks[61] - right_mouth = landmarks[291] - - eye_distance = np.linalg.norm(left_eye - right_eye) - mouth_width = np.linalg.norm(left_mouth - right_mouth) - nose_to_mouth = np.linalg.norm(nose_tip - (left_mouth + right_mouth) / 2) - - # Calculate face shape features - face_width = np.max(landmarks[:, 0]) - np.min(landmarks[:, 0]) - face_height = np.max(landmarks[:, 1]) - np.min(landmarks[:, 1]) - face_depth = np.max(landmarks[:, 2]) - np.min(landmarks[:, 2]) - - # Combine all features into a single embedding - embedding = np.concatenate([ - mean, std, median, min_vals, max_vals, - [eye_distance, mouth_width, nose_to_mouth, face_width, face_height, face_depth] - ]) - - return embedding.tolist() - -embedder = mediapipeEmbedder(MODEL_PATH) - -def process_image(image_data, filename): - try: - results, annotated_img = embedder.process_image(image_data) - - if results: - # Save annotated image - annotated_filename = f"mediapipe_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return results, annotated_filename - else: - print(f"No face landmarks detected in image: {filename}") - return None, None - except Exception as e: - print(f"Error processing image {filename}: {str(e)}") - import traceback - traceback.print_exc() - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"mediapipe_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - annotated_filename = f"mediapipe_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - if frame_count % fps == 0: - frame_results, annotated_frame = embedder.process_image(cv2.imencode('.jpg', frame)[1].tobytes()) - if frame_results: - results.append({"frame": frame_count, "results": frame_results}) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@mediapipe_app.post("/upload") -async def upload_file(file: UploadFile = File(...),api_key: str = Depends(get_api_key)): - # 验证 API key - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - # 检查并更新 token 使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "mediapipe" - await update_token_usage(api_key, tokens_required, model_name) - - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - redis_key = f"mediapipe_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@mediapipe_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_mediapipe_result(filename: str): - redis_key = f"mediapipe_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) - else: - raise HTTPException(status_code=404, detail="Result not found") - -@mediapipe_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"mediapipe_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"mediapipe_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - results, annotated_filename = process_video(content, filename) - - if results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "results": results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:mediapipe_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"mediapipe_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - -if __name__ == "__main__": - threading.Thread(target=process_task, daemon=True).start() - threading.Thread(target=listen_redis_changes, daemon=True).start() - uvicorn.run(app, host="0.0.0.0", port=7006) \ No newline at end of file diff --git a/api_old/before/mediapipe_key2.py b/api_old/before/mediapipe_key2.py deleted file mode 100644 index ecc8ce4..0000000 --- a/api_old/before/mediapipe_key2.py +++ /dev/null @@ -1,462 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import mediapipe as mp -from mediapipe.tasks import python -from mediapipe.tasks.python import vision -import string - -app = FastAPI() -mediapipe_app = FastAPI() -app.mount("/mediapipe", mediapipe_app) - -# CORS configuration -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Configuration -MODEL_PATH = "/home/zydi/models/face_landmarker.task" # Replace with your model path -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "mediapipe" -KAFKA_GROUP_ID = "mediapipe_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 10 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# Ensure directories exist -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# Initialize Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# Initialize Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class mediapipeEmbedder: - def __init__(self, model_path): - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions( - base_options=base_options, - output_face_blendshapes=True, - output_facial_transformation_matrixes=True, - num_faces=1 - ) - self.detector = vision.FaceLandmarker.create_from_options(options) - - def get_mediapipe_landmarks(self, image): - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image) - detection_result = self.detector.detect(mp_image) - if detection_result.face_landmarks: - return np.array([(lm.x, lm.y, lm.z) for lm in detection_result.face_landmarks[0]]) - return None - - def process_image(self, image_data): - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - landmarks = self.get_mediapipe_landmarks(img) - - if landmarks is not None: - # Calculate a more detailed mediapipe embedding - embedding = self.calculate_detailed_embedding(landmarks) - - # Draw landmarks on the image - for lm in landmarks: - cv2.circle(img, (int(lm[0]*img.shape[1]), int(lm[1]*img.shape[0])), 2, (0,255,0), -1) - - return { - "embedding": embedding, - "landmarks": landmarks.tolist() - }, img - else: - return None, img - - def calculate_detailed_embedding(self, landmarks): - # Calculate various statistical features - mean = np.mean(landmarks, axis=0) - std = np.std(landmarks, axis=0) - median = np.median(landmarks, axis=0) - min_vals = np.min(landmarks, axis=0) - max_vals = np.max(landmarks, axis=0) - - # Calculate pairwise distances between key facial landmarks - nose_tip = landmarks[4] - left_eye = landmarks[159] - right_eye = landmarks[386] - left_mouth = landmarks[61] - right_mouth = landmarks[291] - - eye_distance = np.linalg.norm(left_eye - right_eye) - mouth_width = np.linalg.norm(left_mouth - right_mouth) - nose_to_mouth = np.linalg.norm(nose_tip - (left_mouth + right_mouth) / 2) - - # Calculate face shape features - face_width = np.max(landmarks[:, 0]) - np.min(landmarks[:, 0]) - face_height = np.max(landmarks[:, 1]) - np.min(landmarks[:, 1]) - face_depth = np.max(landmarks[:, 2]) - np.min(landmarks[:, 2]) - - # Combine all features into a single embedding - embedding = np.concatenate([ - mean, std, median, min_vals, max_vals, - [eye_distance, mouth_width, nose_to_mouth, face_width, face_height, face_depth] - ]) - - return embedding.tolist() - -embedder = mediapipeEmbedder(MODEL_PATH) - -def process_image(image_data, filename): - try: - results, annotated_img = embedder.process_image(image_data) - - if results: - # Save annotated image - annotated_filename = f"mediapipe_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return results, annotated_filename - else: - print(f"No face landmarks detected in image: {filename}") - return None, None - except Exception as e: - print(f"Error processing image {filename}: {str(e)}") - import traceback - traceback.print_exc() - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"mediapipe_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - annotated_filename = f"mediapipe_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - if frame_count % fps == 0: - frame_results, annotated_frame = embedder.process_image(cv2.imencode('.jpg', frame)[1].tobytes()) - if frame_results: - results.append({"frame": frame_count, "results": frame_results}) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@mediapipe_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "mediapipe" - await update_token_usage(api_key, tokens_required, model_name) - - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - redis_key = f"mediapipe_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@mediapipe_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_mediapipe_result(filename: str): - redis_key = f"mediapipe_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) - else: - raise HTTPException(status_code=404, detail="Result not found") - -@mediapipe_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"mediapipe_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"mediapipe_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - results, annotated_filename = process_video(content, filename) - - if results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "results": results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:mediapipe_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"mediapipe_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - -if __name__ == "__main__": - threading.Thread(target=process_task, daemon=True).start() - threading.Thread(target=listen_redis_changes, daemon=True).start() - uvicorn.run(app, host="0.0.0.0", port=7006) \ No newline at end of file diff --git a/api_old/before/pose_api.py b/api_old/before/pose_api.py deleted file mode 100644 index fa90e34..0000000 --- a/api_old/before/pose_api.py +++ /dev/null @@ -1,312 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.middleware.cors import CORSMiddleware -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import io -import uuid -import os -from datetime import datetime, timedelta -import threading -import torch -torch.cuda.set_device(1) - - -app = FastAPI() -pose_app = FastAPI() -app.mount("/pose", pose_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8x-pose.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "pose" # 指定Kafka topic -KAFKA_GROUP_ID = "pose_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 3 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -class PoseDetector: - def __init__(self, model_path): - self.model = YOLO(model_path).to('cuda:1') - - def detect(self, frame): - results = self.model(frame, device='cuda:1') - return results - - def format_results(self, results): - formatted_results = [] - for r in results: - boxes = r.boxes - keypoints = r.keypoints - for i in range(len(boxes)): - box = boxes[i] - kpts = keypoints[i] - formatted_results.append({ - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "keypoints": kpts.xy.tolist()[0] - }) - return formatted_results - - def draw_results(self, frame, results, original_shape): - for r in results: - annotated_frame = r.plot(img=frame) - # 调整坐标以适应原始图像大小 - h, w = annotated_frame.shape[:2] - scale_x, scale_y = original_shape[1] / w, original_shape[0] / h - annotated_frame = cv2.resize(annotated_frame, (original_shape[1], original_shape[0])) - return annotated_frame - -detector = PoseDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = img.shape - # Convert BGR to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Resize image to fit model requirements (640x640) - img_resized = cv2.resize(img, (640, 640)) - - # Normalize and reshape to BCHW format - img_tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - img_tensor = img_tensor.to('cuda:1') - - results = detector.detect(img_tensor) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on original image - annotated_img = detector.draw_results(img_resized, results, original_shape) - - # Save annotated image - annotated_filename = f"pose_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, cv2.cvtColor(annotated_img, cv2.COLOR_RGB2BGR)) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"pose_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - # Create output video file - annotated_filename = f"pose_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process one frame per second - if frame_count % fps == 0: - # Convert BGR to RGB - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Resize frame to fit model requirements (640x640) - frame_resized = cv2.resize(frame_rgb, (640, 640)) - - # Normalize and reshape to BCHW format - frame_tensor = torch.from_numpy(frame_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - frame_tensor = frame_tensor.to('cuda:1') - - results = detector.detect(frame_tensor) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - # Draw results on original frame - annotated_frame = detector.draw_results(frame_resized, results, original_shape) - # Convert RGB back to BGR for OpenCV - annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@pose_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"pose_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return JSONResponse(content={"message": "File uploaded and queued for processing", "filename": new_filename}) - -@pose_app.get("/result/{filename}") -async def get_pose_result(filename: str): - redis_key = f"pose_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@pose_app.get("/annotated/{filename}") -async def get_annotated_file(filename: str): - redis_key = f"pose_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"pose_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:pose_result:*') # 监听所有pose_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"pose_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7001) \ No newline at end of file diff --git a/api_old/before/pose_key.py b/api_old/before/pose_key.py deleted file mode 100644 index 4d898c2..0000000 --- a/api_old/before/pose_key.py +++ /dev/null @@ -1,461 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import torch -torch.cuda.set_device(1) - - -app = FastAPI() -pose_app = FastAPI() -app.mount("/pose", pose_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8x-pose.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "pose" # 指定Kafka topic -KAFKA_GROUP_ID = "pose_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 3 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class PoseDetector: - def __init__(self, model_path): - self.model = YOLO(model_path).to('cuda:1') - - def detect(self, frame): - results = self.model(frame, device='cuda:1') - return results - - def format_results(self, results): - formatted_results = [] - for r in results: - boxes = r.boxes - keypoints = r.keypoints - for i in range(len(boxes)): - box = boxes[i] - kpts = keypoints[i] - formatted_results.append({ - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "keypoints": kpts.xy.tolist()[0] - }) - return formatted_results - - def draw_results(self, frame, results, original_shape): - for r in results: - annotated_frame = r.plot(img=frame) - # 调整坐标以适应原始图像大小 - h, w = annotated_frame.shape[:2] - scale_x, scale_y = original_shape[1] / w, original_shape[0] / h - annotated_frame = cv2.resize(annotated_frame, (original_shape[1], original_shape[0])) - return annotated_frame - -detector = PoseDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = img.shape - # Convert BGR to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Resize image to fit model requirements (640x640) - img_resized = cv2.resize(img, (640, 640)) - - # Normalize and reshape to BCHW format - img_tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - img_tensor = img_tensor.to('cuda:1') - - results = detector.detect(img_tensor) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on original image - annotated_img = detector.draw_results(img_resized, results, original_shape) - - # Save annotated image - annotated_filename = f"pose_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, cv2.cvtColor(annotated_img, cv2.COLOR_RGB2BGR)) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"pose_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - # Create output video file - annotated_filename = f"pose_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process one frame per second - if frame_count % fps == 0: - # Convert BGR to RGB - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Resize frame to fit model requirements (640x640) - frame_resized = cv2.resize(frame_rgb, (640, 640)) - - # Normalize and reshape to BCHW format - frame_tensor = torch.from_numpy(frame_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - frame_tensor = frame_tensor.to('cuda:1') - - results = detector.detect(frame_tensor) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - # Draw results on original frame - annotated_frame = detector.draw_results(frame_resized, results, original_shape) - # Convert RGB back to BGR for OpenCV - annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@pose_app.post("/upload") -async def upload_file(file: UploadFile = File(...),api_key: str = Depends(get_api_key)): - # 验证 API key - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - # 检查并更新 token 使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8x-pose" - await update_token_usage(api_key, tokens_required, model_name) - - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"pose_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@pose_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_pose_result(filename: str): - redis_key = f"pose_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@pose_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"pose_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"pose_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:pose_result:*') # 监听所有pose_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"pose_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7001) \ No newline at end of file diff --git a/api_old/before/pose_key2.py b/api_old/before/pose_key2.py deleted file mode 100644 index 6f9e599..0000000 --- a/api_old/before/pose_key2.py +++ /dev/null @@ -1,470 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import uuid -import os -from datetime import datetime, timedelta, timezone -import threading -import torch -import string - -torch.cuda.set_device(1) - -app = FastAPI() -pose_app = FastAPI() -app.mount("/pose", pose_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8x-pose.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "pose" # 指定Kafka topic -KAFKA_GROUP_ID = "pose_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 3 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class PoseDetector: - def __init__(self, model_path): - self.model = YOLO(model_path).to('cuda:1') - - def detect(self, frame): - results = self.model(frame, device='cuda:1') - return results - - def format_results(self, results): - formatted_results = [] - for r in results: - boxes = r.boxes - keypoints = r.keypoints - for i in range(len(boxes)): - box = boxes[i] - kpts = keypoints[i] - formatted_results.append({ - "bbox": box.xyxy.tolist()[0], - "confidence": box.conf.item(), - "keypoints": kpts.xy.tolist()[0] - }) - return formatted_results - - def draw_results(self, frame, results, original_shape): - for r in results: - annotated_frame = r.plot(img=frame) - # 调整坐标以适应原始图像大小 - h, w = annotated_frame.shape[:2] - scale_x, scale_y = original_shape[1] / w, original_shape[0] / h - annotated_frame = cv2.resize(annotated_frame, (original_shape[1], original_shape[0])) - return annotated_frame - -detector = PoseDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = img.shape - # Convert BGR to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Resize image to fit model requirements (640x640) - img_resized = cv2.resize(img, (640, 640)) - - # Normalize and reshape to BCHW format - img_tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - img_tensor = img_tensor.to('cuda:1') - - results = detector.detect(img_tensor) - - # Format results for JSON - json_results = detector.format_results(results) - - # Draw results on original image - annotated_img = detector.draw_results(img_resized, results, original_shape) - - # Save annotated image - annotated_filename = f"pose_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, cv2.cvtColor(annotated_img, cv2.COLOR_RGB2BGR)) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing image: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"pose_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - # Get video properties - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - # Create output video file - annotated_filename = f"pose_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process one frame per second - if frame_count % fps == 0: - # Convert BGR to RGB - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Resize frame to fit model requirements (640x640) - frame_resized = cv2.resize(frame_rgb, (640, 640)) - - # Normalize and reshape to BCHW format - frame_tensor = torch.from_numpy(frame_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 - frame_tensor = frame_tensor.to('cuda:1') - - results = detector.detect(frame_tensor) - frame_json_results = detector.format_results(results) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - # Draw results on original frame - annotated_frame = detector.draw_results(frame_resized, results, original_shape) - # Convert RGB back to BGR for OpenCV - annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR) - else: - annotated_frame = frame - - out.write(annotated_frame) - frame_count += 1 - - cap.release() - out.release() - - # Clean up temporary input video file - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"Error processing video: {str(e)}") - return None, None - -@pose_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8x-pose" - await update_token_usage(api_key, tokens_required, model_name) - - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"pose_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@pose_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_pose_result(filename: str): - redis_key = f"pose_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@pose_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"pose_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"pose_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:pose_result:*') # 监听所有pose_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"pose_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7001) \ No newline at end of file diff --git a/api_old/before/qwenvl_api.py b/api_old/before/qwenvl_api.py deleted file mode 100644 index 5cf7a90..0000000 --- a/api_old/before/qwenvl_api.py +++ /dev/null @@ -1,356 +0,0 @@ -import os -import json -import uuid -from datetime import datetime, timedelta -from fastapi import FastAPI, HTTPException, UploadFile, File -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from kafka import KafkaProducer, KafkaConsumer -from transformers import Qwen2VLForConditionalGeneration, AutoTokenizer, AutoProcessor -from qwen_vl_utils import process_vision_info -from decord import VideoReader, cpu -from PIL import Image -from redis import Redis -import io -import re -import torch -from contextlib import asynccontextmanager -import threading - -app = FastAPI() -qwenvl_app = FastAPI() -app.mount("/qwenvl", qwenvl_app) -torch.cuda.set_device(1) -# CORS设置 -ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/qwen/Qwen2-VL-2B-Instruct" -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "qwenvl" -KAFKA_GROUP_ID = "qwenvl_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 8 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - - -# 初始化模型 -model = Qwen2VLForConditionalGeneration.from_pretrained( - MODEL_PATH, torch_dtype="auto", device_map="cuda:1" -) - -min_pixels = 128*28*28 -max_pixels = 512*28*28 -processor = AutoProcessor.from_pretrained(MODEL_PATH, min_pixels=min_pixels, max_pixels=max_pixels) - -class MediaAnalysisSystem: - def __init__(self, model, processor): - self.model = model - self.processor = processor - self.MAX_NUM_FRAMES = 10 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_media(self, media_data, object_name, media_type='image'): - if not media_data: - raise ValueError(f"Empty {media_type} data for {object_name}") - - print(f"Processing {media_type}: {object_name}, data size: {len(media_data)} bytes") - - if media_type == 'video': - frames = self.encode_video(media_data) - media_content = {"type": "video", "video": frames, "fps": 1.0} - else: # image - image = Image.open(io.BytesIO(media_data)) - media_content = {"type": "image", "image": image} - - messages = [ - { - "role": "user", - "content": [ - media_content, - {"type": "text", "text": "用中文尽可能详细地描述这个" + ("视频" if media_type == "video" else "图片") + ",包括场景、人物数量、行为变化等。"}, - ], - } - ] - - text = self.processor.apply_chat_template( - messages, tokenize=False, add_generation_prompt=True - ) - image_inputs, video_inputs = process_vision_info(messages) - inputs = self.processor( - text=[text], - images=image_inputs, - videos=video_inputs, - padding=True, - return_tensors="pt", - ) - inputs = inputs.to('cuda:1') - generated_ids = self.model.generate(**inputs, max_new_tokens=512) - generated_ids_trimmed = [ - out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) - ] - answer = self.processor.batch_decode( - generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False - )[0] - - extracted_info = self.extract_info(answer) - - result = { - "original_answer": answer, - "extracted_info": extracted_info, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - if media_type == 'video': - result["num_frames"] = len(frames) - - return result - - def process_video(self, video_data, object_name): - return self.process_media(video_data, object_name, media_type='video') - - def process_image(self, image_data, object_name): - return self.process_media(image_data, object_name, media_type='image') - - @staticmethod - def extract_time_from_filename(object_name): - filename = os.path.basename(object_name) - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for item_list, key in [(actions, "actions"), (interactions, "interactions"), (objects, "objects"), (furniture, "furniture")]: - for item in item_list: - if item in answer: - info[key].append(item) - - return info - -# 初始化 MediaAnalysisSystem -media_analysis_system = MediaAnalysisSystem(model, processor) - -async def process_file(file: UploadFile, file_type: str): - content = await file.read() - # 获取原始文件的后缀 - original_extension = os.path.splitext(file.filename)[1] - - # 生成新的文件名,包含 UUID 和原始后缀 - filename = f"qwenvl_{uuid.uuid4()}{original_extension}" - file_path = os.path.join(UPLOAD_DIR, filename) - with open(file_path, "wb") as f: - f.write(content) - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": filename, - "type": file_type - }).encode('utf-8')) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return {"message": f"{file_type.capitalize()} uploaded and queued for processing", "filename": filename} - -@qwenvl_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - try: - file_type = "image" if file.content_type.startswith("image") else "video" - return await process_file(file, file_type) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@qwenvl_app.post("/analyze_video") -async def analyze_video(file: UploadFile = File(...)): - try: - return await process_file(file, "video") - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@qwenvl_app.post("/analyze_image") -async def analyze_image(file: UploadFile = File(...)): - try: - return await process_file(file, "image") - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -def process_task(): - for message in consumer: - try: - if isinstance(message.value, dict): - task = message.value - else: - task = json.loads(message.value.decode('utf-8')) - - filename = task['filename'] - file_type = task['type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - with open(file_path, 'rb') as f: - file_data = f.read() - - if file_type == "video": - result = media_analysis_system.process_video(file_data, filename) - elif file_type == "image": - result = media_analysis_system.process_image(file_data, filename) - - # 保存结果到 JSON 文件 - result_file_path = os.path.join(RESULT_DIR, f"{filename}.json") - with open(result_file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - # 将结果存储在 Redis 中 - redis_client.set(redis_key, json.dumps({ - "status": "completed", - "result": result - })) - - except Exception as e: - print(f"Error processing task: {str(e)}") - if 'filename' in locals() and 'file_type' in locals(): - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - else: - print("Error occurred before task details were extracted") - -@qwenvl_app.get("/result/{filename}") -async def get_result(filename: str): - for file_type in ["video", "image"]: - redis_key = f"{file_type}_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_json = json.loads(result) - - if result_json.get("status") == "queued": - return {"status": "queued", "message": "Your request is in the queue and will be processed soon."} - elif result_json.get("status") == "processing": - return {"status": "processing", "message": "Your request is being processed."} - else: - return result_json - - raise HTTPException(status_code=404, detail="Result not found") - -async def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@5__:*_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - print(f"Key changed: {key}") - -if __name__ == "__main__": - # 在后台线程中启动Kafka消费者 - consumer_thread = threading.Thread(target=process_task, daemon=True) - consumer_thread.start() - - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7005) \ No newline at end of file diff --git a/api_old/before/qwenvl_key.py b/api_old/before/qwenvl_key.py deleted file mode 100644 index d730b5e..0000000 --- a/api_old/before/qwenvl_key.py +++ /dev/null @@ -1,508 +0,0 @@ -import os -import json -import uuid -from datetime import datetime, timedelta, timezone -from fastapi import FastAPI, HTTPException, UploadFile, File, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.security import APIKeyHeader -from kafka import KafkaProducer, KafkaConsumer -from transformers import Qwen2VLForConditionalGeneration, AutoTokenizer, AutoProcessor -from qwen_vl_utils import process_vision_info -from decord import VideoReader, cpu -from PIL import Image -from redis import Redis -import io -import re -import torch -from contextlib import asynccontextmanager -import threading - -app = FastAPI() -qwenvl_app = FastAPI() -app.mount("/qwenvl", qwenvl_app) -torch.cuda.set_device(1) - -# CORS设置 -ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/qwen/Qwen2-VL-2B-Instruct" -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "qwenvl" -KAFKA_GROUP_ID = "qwenvl_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 8 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) - -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = Image.open(file_path) - width, height = img.size - pixel_count = width * height - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - vr = VideoReader(file_path) - fps = vr.get_avg_fps() - frame_count = len(vr) - width, height = vr[0].shape[1], vr[0].shape[0] - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - -# 初始化模型 -model = Qwen2VLForConditionalGeneration.from_pretrained( - MODEL_PATH, torch_dtype="auto", device_map="cuda:1" -) - -min_pixels = 128*28*28 -max_pixels = 512*28*28 -processor = AutoProcessor.from_pretrained(MODEL_PATH, min_pixels=min_pixels, max_pixels=max_pixels) - -class MediaAnalysisSystem: - def __init__(self, model, processor): - self.model = model - self.processor = processor - self.MAX_NUM_FRAMES = 10 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_media(self, media_data, object_name, media_type='image'): - if not media_data: - raise ValueError(f"Empty {media_type} data for {object_name}") - - print(f"Processing {media_type}: {object_name}, data size: {len(media_data)} bytes") - - if media_type == 'video': - frames = self.encode_video(media_data) - media_content = {"type": "video", "video": frames, "fps": 1.0} - else: # image - image = Image.open(io.BytesIO(media_data)) - media_content = {"type": "image", "image": image} - - messages = [ - { - "role": "user", - "content": [ - media_content, - {"type": "text", "text": "用中文尽可能详细地描述这个" + ("视频" if media_type == "video" else "图片") + ",包括场景、人物数量、行为变化等。"}, - ], - } - ] - - text = self.processor.apply_chat_template( - messages, tokenize=False, add_generation_prompt=True - ) - image_inputs, video_inputs = process_vision_info(messages) - inputs = self.processor( - text=[text], - images=image_inputs, - videos=video_inputs, - padding=True, - return_tensors="pt", - ) - inputs = inputs.to('cuda:1') - generated_ids = self.model.generate(**inputs, max_new_tokens=512) - generated_ids_trimmed = [ - out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) - ] - answer = self.processor.batch_decode( - generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False - )[0] - - extracted_info = self.extract_info(answer) - - result = { - "original_answer": answer, - "extracted_info": extracted_info, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - if media_type == 'video': - result["num_frames"] = len(frames) - - return result - - def process_video(self, video_data, object_name): - return self.process_media(video_data, object_name, media_type='video') - - def process_image(self, image_data, object_name): - return self.process_media(image_data, object_name, media_type='image') - - @staticmethod - def extract_time_from_filename(object_name): - filename = os.path.basename(object_name) - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for item_list, key in [(actions, "actions"), (interactions, "interactions"), (objects, "objects"), (furniture, "furniture")]: - for item in item_list: - if item in answer: - info[key].append(item) - - return info - -# 初始化 MediaAnalysisSystem -media_analysis_system = MediaAnalysisSystem(model, processor) - -async def process_file(file: UploadFile, file_type: str, api_key: str): - content = await file.read() - original_extension = os.path.splitext(file.filename)[1] - - filename = f"qwenvl_{uuid.uuid4()}{original_extension}" - file_path = os.path.join(UPLOAD_DIR, filename) - with open(file_path, "wb") as f: - f.write(content) - - # 计算token - tokens_required = calculate_tokens(file_path, file_type) - - # 检查并更新token使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新token使用量 - model_name = "Qwen2-VL-2B-Instruct" - await update_token_usage(api_key, tokens_required, model_name) - - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": filename, - "type": file_type - }).encode('utf-8')) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@qwenvl_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - file_type = "image" if file.content_type.startswith("image") else "video" - return await process_file(file, file_type, api_key) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@qwenvl_app.post("/analyze_video") -async def analyze_video(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "video", api_key) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@qwenvl_app.post("/analyze_image") -async def analyze_image(file: UploadFile = File(...), api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "image", api_key) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -def process_task(): - for message in consumer: - try: - if isinstance(message.value, dict): - task = message.value - else: - task = json.loads(message.value.decode('utf-8')) - - filename = task['filename'] - file_type = task['type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - with open(file_path, 'rb') as f: - file_data = f.read() - - if file_type == "video": - result = media_analysis_system.process_video(file_data, filename) - elif file_type == "image": - result = media_analysis_system.process_image(file_data, filename) - - # 保存结果到 JSON 文件 - result_file_path = os.path.join(RESULT_DIR, f"{filename}.json") - with open(result_file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - # 将结果存储在 Redis 中 - redis_client.set(redis_key, json.dumps({ - "status": "completed", - "result": result - })) - - except Exception as e: - print(f"Error processing task: {str(e)}") - if 'filename' in locals() and 'file_type' in locals(): - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - else: - print("Error occurred before task details were extracted") - -@qwenvl_app.get("/result/{filename}") -async def get_result(filename: str, api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - for file_type in ["video", "image"]: - redis_key = f"{file_type}_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_json = json.loads(result) - - if result_json.get("status") == "queued": - return {"status": "queued", "message": "Your request is in the queue and will be processed soon."} - elif result_json.get("status") == "processing": - return {"status": "processing", "message": "Your request is being processed."} - else: - return result_json - - raise HTTPException(status_code=404, detail="Result not found") - - -async def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@5__:*_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - print(f"Key changed: {key}") - -if __name__ == "__main__": - # 在后台线程中启动Kafka消费者 - consumer_thread = threading.Thread(target=process_task, daemon=True) - consumer_thread.start() - - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7005) \ No newline at end of file diff --git a/api_old/before/qwenvl_key2.py b/api_old/before/qwenvl_key2.py deleted file mode 100644 index 06b187e..0000000 --- a/api_old/before/qwenvl_key2.py +++ /dev/null @@ -1,518 +0,0 @@ -import os -import json -import uuid -from datetime import datetime, timedelta, timezone -from fastapi import FastAPI, HTTPException, UploadFile, File, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.security import APIKeyHeader -from kafka import KafkaProducer, KafkaConsumer -from transformers import Qwen2VLForConditionalGeneration, AutoTokenizer, AutoProcessor -from qwen_vl_utils import process_vision_info -from decord import VideoReader, cpu -from PIL import Image -from redis import Redis -import io -import re -import torch -from contextlib import asynccontextmanager -import threading -import string - - -app = FastAPI() -qwenvl_app = FastAPI() -app.mount("/qwenvl", qwenvl_app) -torch.cuda.set_device(1) - -# CORS设置 -ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/qwen/Qwen2-VL-2B-Instruct" -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "qwenvl" -KAFKA_GROUP_ID = "qwenvl_group" - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 8 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) - -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = Image.open(file_path) - width, height = img.size - pixel_count = width * height - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - vr = VideoReader(file_path) - fps = vr.get_avg_fps() - frame_count = len(vr) - width, height = vr[0].shape[1], vr[0].shape[0] - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - -# 初始化模型 -model = Qwen2VLForConditionalGeneration.from_pretrained( - MODEL_PATH, torch_dtype="auto", device_map="cuda:1" -) - -min_pixels = 128*28*28 -max_pixels = 512*28*28 -processor = AutoProcessor.from_pretrained(MODEL_PATH, min_pixels=min_pixels, max_pixels=max_pixels) - -class MediaAnalysisSystem: - def __init__(self, model, processor): - self.model = model - self.processor = processor - self.MAX_NUM_FRAMES = 10 - - def encode_video(self, video_data): - def uniform_sample(l, n): - gap = len(l) / n - return [l[int(i * gap + gap / 2)] for i in range(n)] - - video_file = io.BytesIO(video_data) - vr = VideoReader(video_file) - sample_fps = round(vr.get_avg_fps() / 1) - frame_idx = list(range(0, len(vr), sample_fps)) - if len(frame_idx) > self.MAX_NUM_FRAMES: - frame_idx = uniform_sample(frame_idx, self.MAX_NUM_FRAMES) - frames = vr.get_batch(frame_idx).asnumpy() - frames = [Image.fromarray(v.astype('uint8')) for v in frames] - print('num frames:', len(frames)) - return frames - - def process_media(self, media_data, object_name, media_type='image'): - if not media_data: - raise ValueError(f"Empty {media_type} data for {object_name}") - - print(f"Processing {media_type}: {object_name}, data size: {len(media_data)} bytes") - - if media_type == 'video': - frames = self.encode_video(media_data) - media_content = {"type": "video", "video": frames, "fps": 1.0} - else: # image - image = Image.open(io.BytesIO(media_data)) - media_content = {"type": "image", "image": image} - - messages = [ - { - "role": "user", - "content": [ - media_content, - {"type": "text", "text": "用中文尽可能详细地描述这个" + ("视频" if media_type == "video" else "图片") + ",包括场景、人物数量、行为变化等。"}, - ], - } - ] - - text = self.processor.apply_chat_template( - messages, tokenize=False, add_generation_prompt=True - ) - image_inputs, video_inputs = process_vision_info(messages) - inputs = self.processor( - text=[text], - images=image_inputs, - videos=video_inputs, - padding=True, - return_tensors="pt", - ) - inputs = inputs.to('cuda:1') - generated_ids = self.model.generate(**inputs, max_new_tokens=512) - generated_ids_trimmed = [ - out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) - ] - answer = self.processor.batch_decode( - generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False - )[0] - - extracted_info = self.extract_info(answer) - - result = { - "original_answer": answer, - "extracted_info": extracted_info, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - if media_type == 'video': - result["num_frames"] = len(frames) - - return result - - def process_video(self, video_data, object_name): - return self.process_media(video_data, object_name, media_type='video') - - def process_image(self, image_data, object_name): - return self.process_media(image_data, object_name, media_type='image') - - @staticmethod - def extract_time_from_filename(object_name): - filename = os.path.basename(object_name) - time_str = filename.split('_')[0] + '_' + filename.split('_')[1].split('.')[0] - - try: - start_time = datetime.strptime(time_str, "%Y%m%d_%H%M%S") - end_time = start_time + timedelta(seconds=10) - return start_time, end_time - except ValueError: - print(f"无法从文件名 '{filename}' 解析时间。使用默认时间。") - return datetime.now(), datetime.now() + timedelta(seconds=10) - - @staticmethod - def extract_info(answer): - info = { - "environment": None, - "num_people": None, - "actions": [], - "interactions": [], - "objects": [], - "furniture": [] - } - - environments = ["办公室", "室内", "室外", "会议室"] - for env in environments: - if env in answer.lower(): - info["environment"] = env - break - - people_patterns = [ - r'(\d+)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一|二|三|四|五|六|七|八|九|十)\s*(人|个人|位|名|员工|用户|小朋友|成年人|女性|男性)', - r'(一个|几个)\s*(人|个人|员工|用户|小朋友|成年人|女性|男性)', - r'几\s*(名|位)\s*(人|员工|用户|小朋友|成年人|女性|男性)?', - r'(男|女)(性|生|士)', - r'(成年|未成年|青少年|老年)\s*(人|群体)', - r'(员工|职工|工人|学生|顾客|观众|游客|乘客)', - r'(群众|民众|大众|公众)', - r'(男女|老少|老幼|大人|小孩)' - ] - for pattern in people_patterns: - match = re.search(pattern, answer) - if match: - if match.group(1).isdigit(): - info["num_people"] = int(match.group(1)) - elif match.group(1) in ['一个', '一']: - info["num_people"] = 1 - else: - num_word_to_digit = { - '二': 2, '三': 3, '四': 4, '五': 5, - '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 - } - info["num_people"] = num_word_to_digit.get(match.group(1), 0) - break - - actions = ["坐", "站", "摔倒", "跳舞", "转身", "摔", "倒", "倒下", "躺下", "转身", "跳跃", "跳", "躺", "睡", "说话"] - interactions = ["互动", "交流", "身体语言", "交谈", "讨论", "开会"] - objects = ["水瓶", "办公用品", "文件", "电脑"] - furniture = ["椅子", "桌子", "咖啡桌", "文件柜", "床", "沙发"] - - for item_list, key in [(actions, "actions"), (interactions, "interactions"), (objects, "objects"), (furniture, "furniture")]: - for item in item_list: - if item in answer: - info[key].append(item) - - return info - -# 初始化 MediaAnalysisSystem -media_analysis_system = MediaAnalysisSystem(model, processor) - -async def process_file(file: UploadFile, file_type: str, api_key_info: dict): - content = await file.read() - original_extension = os.path.splitext(file.filename)[1] - - filename = f"qwenvl_{uuid.uuid4()}{original_extension}" - file_path = os.path.join(UPLOAD_DIR, filename) - with open(file_path, "wb") as f: - f.write(content) - - # 计算token - tokens_required = calculate_tokens(file_path, file_type) - - # 检查并更新token使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新token使用量 - model_name = "Qwen2-VL-2B-Instruct" - await update_token_usage(api_key, tokens_required, model_name) - - - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": filename, - "type": file_type - }).encode('utf-8')) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@qwenvl_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - try: - file_type = "image" if file.content_type.startswith("image") else "video" - return await process_file(file, file_type, api_key_info) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@qwenvl_app.post("/analyze_video") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - api_key_info = await verify_api_key(api_key_info) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "video", api_key_info) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -@qwenvl_app.post("/analyze_image") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - api_key_info = await verify_api_key(api_key_info) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - try: - return await process_file(file, "image", api_key_info) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) - -def process_task(): - for message in consumer: - try: - if isinstance(message.value, dict): - task = message.value - else: - task = json.loads(message.value.decode('utf-8')) - - filename = task['filename'] - file_type = task['type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - with open(file_path, 'rb') as f: - file_data = f.read() - - if file_type == "video": - result = media_analysis_system.process_video(file_data, filename) - elif file_type == "image": - result = media_analysis_system.process_image(file_data, filename) - - # 保存结果到 JSON 文件 - result_file_path = os.path.join(RESULT_DIR, f"{filename}.json") - with open(result_file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - # 将结果存储在 Redis 中 - redis_client.set(redis_key, json.dumps({ - "status": "completed", - "result": result - })) - - except Exception as e: - print(f"Error processing task: {str(e)}") - if 'filename' in locals() and 'file_type' in locals(): - redis_key = f"{file_type}_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - else: - print("Error occurred before task details were extracted") - -@qwenvl_app.get("/result/{filename}") -async def get_result(filename: str, api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - for file_type in ["video", "image"]: - redis_key = f"{file_type}_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_json = json.loads(result) - - if result_json.get("status") == "queued": - return {"status": "queued", "message": "Your request is in the queue and will be processed soon."} - elif result_json.get("status") == "processing": - return {"status": "processing", "message": "Your request is being processed."} - else: - return result_json - - raise HTTPException(status_code=404, detail="Result not found") - - -async def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@5__:*_result:*') - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - print(f"Key changed: {key}") - -if __name__ == "__main__": - # 在后台线程中启动Kafka消费者 - consumer_thread = threading.Thread(target=process_task, daemon=True) - consumer_thread.start() - - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7005) \ No newline at end of file diff --git a/api_old/before/yolo_api.py b/api_old/before/yolo_api.py deleted file mode 100644 index d2d5043..0000000 --- a/api_old/before/yolo_api.py +++ /dev/null @@ -1,315 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.middleware.cors import CORSMiddleware -from ultralytics import YOLO -import cv2 -import numpy as np -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -from redis import Redis -import io -import uuid -import os -from datetime import datetime, timedelta -import threading -import torch -torch.cuda.set_device(1) -import colorsys - -app = FastAPI() -yolo_app = FastAPI() -app.mount("/yolo", yolo_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8x.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "yolo" # 指定Kafka topic -KAFKA_GROUP_ID = "yolo_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 6 - -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -class yoloDetector: - def __init__(self, model_path): - self.model = YOLO(model_path) - - def detect(self, frame): - results = self.model(frame) - return results - - def format_results(self, results, original_shape): - formatted_results = [] - for r in results: - boxes = r.boxes - for box in boxes: - x1, y1, x2, y2 = box.xyxy[0].tolist() - - # 缩放坐标到原始图像尺寸 - x1, x2 = [x * original_shape[1] / 640 for x in [x1, x2]] - y1, y2 = [y * original_shape[0] / 640 for y in [y1, y2]] - - conf = box.conf.item() - cls = int(box.cls.item()) - name = self.model.names[cls] - - formatted_results.append({ - "class": name, - "confidence": conf, - "bbox": [x1, y1, x2, y2] - }) - return formatted_results - - def draw_results(self, frame, formatted_results): - for result in formatted_results: - x1, y1, x2, y2 = map(int, result['bbox']) - name = result['class'] - conf = result['confidence'] - - cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # 使用固定的绿色 - label = f"{name} {conf:.2f}" - (text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2) - cv2.rectangle(frame, (x1, y1 - text_height - 5), (x1 + text_width, y1), (0, 255, 0), -1) - cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) - - return frame - -detector = yoloDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - original_img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = original_img.shape - - img = cv2.cvtColor(original_img, cv2.COLOR_BGR2RGB) - img = cv2.resize(img, (640, 640)) - img = img.transpose((2, 0, 1)) - img = np.ascontiguousarray(img) - img = torch.from_numpy(img).float() - img /= 255.0 - img = img.unsqueeze(0) - - results = detector.detect(img) - - json_results = detector.format_results(results, original_shape) - - annotated_img = detector.draw_results(original_img, json_results) - - annotated_filename = f"yolo_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return json_results, annotated_filename - except Exception as e: - print(f"处理图像时出错: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"yolo_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - annotated_filename = f"yolo_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), 1, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # 每秒只处理一帧 - if frame_count % fps == 0: - preprocessed_frame = preprocess_frame(frame) - - results = detector.detect(preprocessed_frame) - frame_json_results = detector.format_results(results, original_shape) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - annotated_frame = detector.draw_results(frame, frame_json_results) - out.write(annotated_frame) - - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"处理视频时出错: {str(e)}") - return None, None - -def preprocess_frame(frame): - # 预处理单个视频帧 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - frame_resized = cv2.resize(frame_rgb, (640, 640)) # 调整为YOLO输入尺寸 - frame_transposed = frame_resized.transpose((2, 0, 1)) # HWC转为CHW - frame_contiguous = np.ascontiguousarray(frame_transposed) - frame_tensor = torch.from_numpy(frame_contiguous).float() - frame_normalized = frame_tensor / 255.0 # 归一化到[0, 1] - frame_batched = frame_normalized.unsqueeze(0) # 添加批次维度 - return frame_batched - -@yolo_app.post("/upload") -async def upload_file(file: UploadFile = File(...)): - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # Save the original file - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # Send processing task to Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - }).encode('utf-8')) - - # Set initial status in Redis - redis_key = f"yolo_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - return JSONResponse(content={"message": "File uploaded and queued for processing", "filename": new_filename}) - -@yolo_app.get("/result/{filename}") -async def get_yolo_result(filename: str): - redis_key = f"yolo_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@yolo_app.get("/annotated/{filename}") -async def get_annotated_file(filename: str): - redis_key = f"yolo_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"yolo_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:yolo_result:*') # 监听所有yolo_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"yolo_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7003) \ No newline at end of file diff --git a/api_old/before/yolo_key.py b/api_old/before/yolo_key.py deleted file mode 100644 index f4f9721..0000000 --- a/api_old/before/yolo_key.py +++ /dev/null @@ -1,462 +0,0 @@ -import os -import cv2 -import torch -import numpy as np -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Header -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from redis import Redis -from ultralytics import YOLO -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -import uuid -from datetime import datetime, timedelta, timezone -import threading - - -app = FastAPI() -yolo_app = FastAPI() -app.mount("/yolo", yolo_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8x.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "yolo" # 指定Kafka topic -KAFKA_GROUP_ID = "yolo_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 6 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 添加API密钥验证 -API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) - -async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)): - if api_key is None: - raise HTTPException(status_code=400, detail="API密钥缺失") - return api_key - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - return None - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - return None - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - return None - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class yoloDetector: - def __init__(self, model_path): - self.model = YOLO(model_path) - - def detect(self, frame): - results = self.model(frame) - return results - - def format_results(self, results, original_shape): - formatted_results = [] - for r in results: - boxes = r.boxes - for box in boxes: - x1, y1, x2, y2 = box.xyxy[0].tolist() - - # 缩放坐标到原始图像尺寸 - x1, x2 = [x * original_shape[1] / 640 for x in [x1, x2]] - y1, y2 = [y * original_shape[0] / 640 for y in [y1, y2]] - - conf = box.conf.item() - cls = int(box.cls.item()) - name = self.model.names[cls] - - formatted_results.append({ - "class": name, - "confidence": conf, - "bbox": [x1, y1, x2, y2] - }) - return formatted_results - - def draw_results(self, frame, formatted_results): - for result in formatted_results: - x1, y1, x2, y2 = map(int, result['bbox']) - name = result['class'] - conf = result['confidence'] - - cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # 使用固定的绿色 - label = f"{name} {conf:.2f}" - (text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2) - cv2.rectangle(frame, (x1, y1 - text_height - 5), (x1 + text_width, y1), (0, 255, 0), -1) - cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) - - return frame - -detector = yoloDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - original_img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = original_img.shape - - img = cv2.cvtColor(original_img, cv2.COLOR_BGR2RGB) - img = cv2.resize(img, (640, 640)) - img = img.transpose((2, 0, 1)) - img = np.ascontiguousarray(img) - img = torch.from_numpy(img).float() - img /= 255.0 - img = img.unsqueeze(0) - - results = detector.detect(img) - - json_results = detector.format_results(results, original_shape) - - annotated_img = detector.draw_results(original_img, json_results) - - annotated_filename = f"yolo_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return json_results, annotated_filename - except Exception as e: - print(f"处理图像时出错: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"yolo_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - annotated_filename = f"yolo_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), 1, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # 每秒只处理一帧 - if frame_count % fps == 0: - preprocessed_frame = preprocess_frame(frame) - - results = detector.detect(preprocessed_frame) - frame_json_results = detector.format_results(results, original_shape) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - annotated_frame = detector.draw_results(frame, frame_json_results) - out.write(annotated_frame) - - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"处理视频时出错: {str(e)}") - return None, None - -def preprocess_frame(frame): - # 预处理单个视频帧 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - frame_resized = cv2.resize(frame_rgb, (640, 640)) # 调整为YOLO输入尺寸 - frame_transposed = frame_resized.transpose((2, 0, 1)) # HWC转为CHW - frame_contiguous = np.ascontiguousarray(frame_transposed) - frame_tensor = torch.from_numpy(frame_contiguous).float() - frame_normalized = frame_tensor / 255.0 # 归一化到[0, 1] - frame_batched = frame_normalized.unsqueeze(0) # 添加批次维度 - return frame_batched - -@yolo_app.post("/upload") -async def upload_file(file: UploadFile = File(...),api_key: str = Depends(get_api_key)): - # 验证 API key - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # 保存原始文件 - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - # 检查并更新 token 使用量 - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8x" - await update_token_usage(api_key, tokens_required, model_name) - - - # 发送处理任务到 Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": file_type - }).encode('utf-8')) - - # 在 Redis 中设置初始状态 - redis_key = f"yolo_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) -@yolo_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_yolo_result(filename: str): - redis_key = f"yolo_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@yolo_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"yolo_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"yolo_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:yolo_result:*') # 监听所有yolo_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"yolo_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7003) \ No newline at end of file diff --git a/api_old/before/yolo_key2.py b/api_old/before/yolo_key2.py deleted file mode 100644 index f9ab7e0..0000000 --- a/api_old/before/yolo_key2.py +++ /dev/null @@ -1,472 +0,0 @@ -import os -import cv2 -import torch -import numpy as np -from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse -from fastapi.security import APIKeyHeader -from redis import Redis -from ultralytics import YOLO -import json -import uvicorn -from kafka import KafkaProducer, KafkaConsumer -import uuid -from datetime import datetime, timedelta, timezone -import threading -import string - - -app = FastAPI() -yolo_app = FastAPI() -app.mount("/yolo", yolo_app) - -# CORS配置 -ALLOWED_ORIGINS = 'https://beta.obscura.work' - -# 只为主应用添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -MODEL_PATH = "/home/zydi/models/yolov8x.pt" # 请替换为您的模型路径 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_TOPIC = "yolo" # 指定Kafka topic -KAFKA_GROUP_ID = "yolo_group" # 指定消费者组ID - -REDIS_HOST = "222.186.10.253" -REDIS_PORT = 6379 -REDIS_PASSWORD = "Obscura@2024" -REDIS_DB = 6 -REDIS_API_DB = 12 -REDIS_API_USAGE_DB = 13 -UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload" -RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result" -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 初始化 Kafka -producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) -consumer = KafkaConsumer( - KAFKA_TOPIC, - bootstrap_servers=[KAFKA_BROKER], - group_id=KAFKA_GROUP_ID, - auto_offset_reset='earliest', - enable_auto_commit=True, - value_deserializer=lambda x: json.loads(x.decode('utf-8')) -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = cv2.imread(file_path) - if img is None: - raise ValueError("无法读取图片文件") - height, width = img.shape[:2] - pixel_count = height * width - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - cap = cv2.VideoCapture(file_path) - if not cap.isOpened(): - raise ValueError("无法打开视频文件") - fps = cap.get(cv2.CAP_PROP_FPS) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - cap.release() - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -class yoloDetector: - def __init__(self, model_path): - self.model = YOLO(model_path) - - def detect(self, frame): - results = self.model(frame) - return results - - def format_results(self, results, original_shape): - formatted_results = [] - for r in results: - boxes = r.boxes - for box in boxes: - x1, y1, x2, y2 = box.xyxy[0].tolist() - - # 缩放坐标到原始图像尺寸 - x1, x2 = [x * original_shape[1] / 640 for x in [x1, x2]] - y1, y2 = [y * original_shape[0] / 640 for y in [y1, y2]] - - conf = box.conf.item() - cls = int(box.cls.item()) - name = self.model.names[cls] - - formatted_results.append({ - "class": name, - "confidence": conf, - "bbox": [x1, y1, x2, y2] - }) - return formatted_results - - def draw_results(self, frame, formatted_results): - for result in formatted_results: - x1, y1, x2, y2 = map(int, result['bbox']) - name = result['class'] - conf = result['confidence'] - - cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # 使用固定的绿色 - label = f"{name} {conf:.2f}" - (text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2) - cv2.rectangle(frame, (x1, y1 - text_height - 5), (x1 + text_width, y1), (0, 255, 0), -1) - cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) - - return frame - -detector = yoloDetector(MODEL_PATH) - -def process_image(image_data, filename): - try: - original_img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR) - original_shape = original_img.shape - - img = cv2.cvtColor(original_img, cv2.COLOR_BGR2RGB) - img = cv2.resize(img, (640, 640)) - img = img.transpose((2, 0, 1)) - img = np.ascontiguousarray(img) - img = torch.from_numpy(img).float() - img /= 255.0 - img = img.unsqueeze(0) - - results = detector.detect(img) - - json_results = detector.format_results(results, original_shape) - - annotated_img = detector.draw_results(original_img, json_results) - - annotated_filename = f"yolo_{filename}" - annotated_path = os.path.join(RESULT_DIR, annotated_filename) - cv2.imwrite(annotated_path, annotated_img) - - return json_results, annotated_filename - except Exception as e: - print(f"处理图像时出错: {str(e)}") - return None, None - -def process_video(video_data, filename): - try: - temp_video_path = os.path.join(UPLOAD_DIR, f"yolo_{filename}") - with open(temp_video_path, 'wb') as temp_video: - temp_video.write(video_data) - - cap = cv2.VideoCapture(temp_video_path) - frame_count = 0 - json_results = [] - - fps = int(cap.get(cv2.CAP_PROP_FPS)) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - original_shape = (height, width) - - annotated_filename = f"yolo_{filename}" - output_path = os.path.join(RESULT_DIR, annotated_filename) - out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), 1, (width, height)) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # 每秒只处理一帧 - if frame_count % fps == 0: - preprocessed_frame = preprocess_frame(frame) - - results = detector.detect(preprocessed_frame) - frame_json_results = detector.format_results(results, original_shape) - json_results.append({"frame": frame_count, "detections": frame_json_results}) - - annotated_frame = detector.draw_results(frame, frame_json_results) - out.write(annotated_frame) - - frame_count += 1 - - cap.release() - out.release() - - os.remove(temp_video_path) - - return json_results, annotated_filename - except Exception as e: - print(f"处理视频时出错: {str(e)}") - return None, None - -def preprocess_frame(frame): - # 预处理单个视频帧 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - frame_resized = cv2.resize(frame_rgb, (640, 640)) # 调整为YOLO输入尺寸 - frame_transposed = frame_resized.transpose((2, 0, 1)) # HWC转为CHW - frame_contiguous = np.ascontiguousarray(frame_transposed) - frame_tensor = torch.from_numpy(frame_contiguous).float() - frame_normalized = frame_tensor / 255.0 # 归一化到[0, 1] - frame_batched = frame_normalized.unsqueeze(0) # 添加批次维度 - return frame_batched - -@yolo_app.post("/upload") -async def upload_file(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - # 保存原始文件 - original_file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(original_file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(original_file_path, file_type) - if tokens_required is None or tokens_required <= 0: - raise HTTPException(status_code=500, detail="无法计算所需的token数量") - - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - model_name = "yolov8x" - await update_token_usage(api_key, tokens_required, model_name) - - - # 发送处理任务到 Kafka - producer.send(KAFKA_TOPIC, json.dumps({ - "filename": new_filename, - "file_type": file_type - }).encode('utf-8')) - - # 在 Redis 中设置初始状态 - redis_key = f"yolo_result:{new_filename}" - redis_client.set(redis_key, json.dumps({"status": "queued"})) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return JSONResponse(content={ - "message": "文件已上传并排队等待处理", - "filename": new_filename, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - }) - - -@yolo_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_yolo_result(filename: str): - redis_key = f"yolo_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - return JSONResponse(content=result_data) # 直接返回整个结果,包括 status - else: - raise HTTPException(status_code=404, detail="Result not found") - -@yolo_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)]) -async def get_annotated_file(filename: str): - redis_key = f"yolo_result:{filename}" - result = redis_client.get(redis_key) - if result: - result_data = json.loads(result) - if result_data["status"] == "completed": - annotated_filename = result_data["annotated_filename"] - file_path = os.path.join(RESULT_DIR, annotated_filename) - if os.path.exists(file_path): - def iterfile(): - with open(file_path, mode="rb") as file_like: - yield from file_like - file_extension = os.path.splitext(annotated_filename)[1].lower() - return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4") - - raise HTTPException(status_code=404, detail="Annotated file not found") - -def process_task(): - for message in consumer: - task = message.value - filename = task['filename'] - file_type = task['file_type'] - - file_path = os.path.join(UPLOAD_DIR, filename) - - # Update status to "processing" - redis_key = f"yolo_result:{filename}" - redis_client.set(redis_key, json.dumps({"status": "processing"})) - - try: - if file_type == "image": - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_image(content, filename) - else: - with open(file_path, 'rb') as f: - content = f.read() - json_results, annotated_filename = process_video(content, filename) - - if json_results and annotated_filename: - redis_client.set(redis_key, json.dumps({ - "json_results": json_results, - "status": "completed", - "annotated_filename": annotated_filename - })) - else: - redis_client.set(redis_key, json.dumps({"status": "failed"})) - except Exception as e: - print(f"Error processing task: {str(e)}") - redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)})) - -def listen_redis_changes(): - pubsub = redis_client.pubsub() - pubsub.psubscribe('__keyspace@3__:yolo_result:*') # 监听所有yolo_result键的变化 - - for message in pubsub.listen(): - if message['type'] == 'pmessage': - key = message['channel'].decode('utf-8').split(':')[-1] - operation = message['data'].decode('utf-8') - - if operation == 'set': - value = redis_client.get(f"yolo_result:{key}") - if value: - result = json.loads(value) - print(f"Status update for {key}: {result['status']}") - - # 这里可以添加其他处理逻辑,比如发送通知等 - - -if __name__ == "__main__": - # 启动处理任务的线程 - threading.Thread(target=process_task, daemon=True).start() - - # 启动Redis监听线程 - threading.Thread(target=listen_redis_changes, daemon=True).start() - - uvicorn.run(app, host="0.0.0.0", port=7003) \ No newline at end of file diff --git a/api_old/config.py b/api_old/config.py deleted file mode 100644 index fcf97d6..0000000 --- a/api_old/config.py +++ /dev/null @@ -1,81 +0,0 @@ -# config.py - -import os - -# Kafka配置 -KAFKA_BROKER = "222.186.10.253:9092" -KAFKA_GROUP_ID_PREFIX = "group" - -# Redis配置 -REDIS_HOST = "150.158.144.159" -REDIS_PORT = 13003 -REDIS_PASSWORD = "Obscura@2024" -MAIN_REDIS_DB = 0 -REDIS_API_DB = 2 -REDIS_API_USAGE_DB = 3 -# 目录配置 -UPLOAD_DIR = "/obscura/task/upload" -RESULT_DIR = "/obscura/task/result" - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 模型配置 -YOLO_MODEL_PATH = "/obscura/models/yolov8x.pt" -POSE_MODEL_PATH = "/obscura/models/yolov8x-pose.pt" -QWEN_MODEL_PATH = "/obscura/models/qwen/Qwen2-VL-2B-Instruct" -FALL_MODEL_PATH = "/obscura/models/yolov8n-fall.pt" -FACE_MODEL_PATH = "/obscura/models/yolov8n-face.pt" -MEDIAPIPE_MODEL_PATH = "/obscura/models/face_landmarker.task" -# COMPARE_MODEL_PATH = "/obscura/models/insightface/insw_r100_glint360k.onnx" -# Ollama配置 -OLLAMA_URL = "http://127.0.0.1:11434/api/generate" - -# 各个worker的配置 -WORKER_CONFIGS = { - "yolo": { - "kafka_topic": "yolo", - "redis_db": 4, - }, - "pose": { - "kafka_topic": "pose", - "redis_db": 5, - }, - "qwenvl": { - "kafka_topic": "qwenvl", - "redis_db": 9, - }, - "qwenvl_analyze": { - "kafka_topic": "qwenvl_analyze", - "redis_db": 32, - }, - "cpm": { - "kafka_topic": "cpm", - "redis_db": 8, - }, - "cpm_analyze": { - "kafka_topic": "cpm_analyze", - "redis_db": 31, - }, - "fall": { - "kafka_topic": "fall", - "redis_db": 6, - }, - "face": { - "kafka_topic": "face", - "redis_db": 7, - }, - "mediapipe": { - "kafka_topic": "mediapipe", - "redis_db": 10, - }, - "compare": { - "kafka_topic": "compare", - "redis_db": 30, - } -} - -# GPU设置 -CUDA_DEVICE_0 = "cuda:0" -CUDA_DEVICE_1 = "cuda:1" diff --git a/api_old/producer.py b/api_old/producer.py deleted file mode 100644 index 32a7b46..0000000 --- a/api_old/producer.py +++ /dev/null @@ -1,419 +0,0 @@ -# main.py -from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from fastapi.security import APIKeyHeader -from kafka import KafkaProducer -from redis import Redis -import os -import json -import uuid -from datetime import datetime, timedelta, timezone -import string -from decord import VideoReader -from PIL import Image -from fastapi.responses import FileResponse -import logging -from config import * - -app = FastAPI() -v1_app = FastAPI() -app.mount("/v1", v1_app) - - -# CORS设置 -# ALLOWED_ORIGINS = ['https://beta.obscura.work'] - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -KAFKA_BROKER = KAFKA_BROKER -REDIS_HOST = REDIS_HOST -REDIS_PORT = REDIS_PORT -REDIS_PASSWORD = REDIS_PASSWORD -REDIS_DB = MAIN_REDIS_DB -REDIS_API_DB = REDIS_API_DB -REDIS_API_USAGE_DB = REDIS_API_USAGE_DB -UPLOAD_DIR = UPLOAD_DIR -RESULT_DIR = RESULT_DIR -MAX_FILE_AGE = timedelta(hours=1) - -# 确保目录存在 -os.makedirs(UPLOAD_DIR, exist_ok=True) -os.makedirs(RESULT_DIR, exist_ok=True) - -# 定义支持的任务类型 -KAFKA_TOPICS = { - 'pose': 'pose', - 'mediapipe': 'mediapipe', - 'qwenvl': 'qwenvl', - 'yolo': 'yolo', - 'fall': 'fall', - 'face': 'face', - 'cpm': 'cpm' -} - -TASK_TYPES = list(KAFKA_TOPICS.keys()) - - -# 初始化 Kafka Producer -producer = KafkaProducer( - bootstrap_servers=[KAFKA_BROKER], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# 初始化 Redis -redis_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_DB -) - -redis_api_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_DB -) - -redis_api_usage_client = Redis( - host=REDIS_HOST, - port=REDIS_PORT, - password=REDIS_PASSWORD, - db=REDIS_API_USAGE_DB -) -redis_pose_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['pose']['redis_db']) -redis_cpm_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['cpm']['redis_db']) -redis_yolo_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['yolo']['redis_db']) -redis_face_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['face']['redis_db']) -redis_fall_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['fall']['redis_db']) -redis_mediapipe_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['mediapipe']['redis_db']) -redis_qwenvl_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=WORKER_CONFIGS['qwenvl']['redis_db']) - -@v1_app.get('/favicon.ico', include_in_schema=False) -async def favicon(): - file_name = "favicon.ico" - file_path = os.path.join(app.root_path, "static", file_name) - if os.path.isfile(file_path): - return FileResponse(file_path) - else: - return {"message": "Favicon not found"}, 404 - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -# 定义 base62 字符集 -BASE62 = string.digits + string.ascii_letters - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -async def verify_api_key(api_key: str = Depends(get_api_key)): - logging.info(f"验证API密钥: {api_key}") - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - logging.warning(f"API密钥不存在: {api_key}") - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - logging.warning(f"API密钥已停用: {api_key}") - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - logging.warning(f"API密钥已过期: {api_key}") - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - logging.info(f"API密钥验证成功: {api_key}") - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - # 更新总的token使用量 - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - # 更新特定模型的token使用量 - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -def calculate_tokens(file_path: str, file_type: str) -> int: - base_tokens = 0 - - try: - file_size = os.path.getsize(file_path) # 获取文件大小(字节) - - # 基础token:每MB文件大小消耗10个token - base_tokens = int((file_size / (1024 * 1024)) * 10) - - if file_type == "image": - img = Image.open(file_path) - width, height = img.size - pixel_count = width * height - - # 图片token:每100个像素额外消耗5个token - image_tokens = int((pixel_count / 10000) * 5) - - base_tokens += image_tokens - - elif file_type == "video": - vr = VideoReader(file_path) - fps = vr.get_avg_fps() - frame_count = len(vr) - width, height = vr[0].shape[1], vr[0].shape[0] - - pixel_count = width * height * frame_count - duration = frame_count / fps # 视频时长(秒) - - # 视频token:每100万像素每秒额外消耗1个token - video_tokens = int((pixel_count / 10000) * (duration / 60)) - - base_tokens += video_tokens - - return max(1, base_tokens) # 确保至少返回1个token - except Exception as e: - print(f"计算token时出错: {str(e)}") - return 1 # 出错时返回默认值1 - - - -async def upload_file(file: UploadFile, task_type: str, api_key_info: dict): - if task_type not in KAFKA_TOPICS: - raise HTTPException(status_code=400, detail="不支持的任务类型") - - content = await file.read() - file_extension = os.path.splitext(file.filename)[1].lower() - new_filename = f"{uuid.uuid4()}{file_extension}" - - file_path = os.path.join(UPLOAD_DIR, new_filename) - with open(file_path, "wb") as f: - f.write(content) - - # 计算 token - file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video" - tokens_required = calculate_tokens(file_path, file_type) - - # 检查并更新 token 使用量 - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - await update_token_usage(api_key, tokens_required, task_type) - - # 创建任务记录 - task_id = str(uuid.uuid4()) - task_data = { - "task_id": task_id, - "filename": new_filename, - "file_type": file_type, - "task_type": task_type, - "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), - } - - # 存储任务信息到Redis - redis_client.set(f"task:{task_id}", json.dumps(task_data)) - logging.info(f"任务信息已存储到Redis: {task_id}") - - # 发送任务到对应的Kafka主题 - kafka_topic = KAFKA_TOPICS[task_type] - producer.send(kafka_topic, task_data) - logging.info(f"任务已发送到Kafka主题: {kafka_topic}") - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{task_type}_tokens_used", 0)) - - response_data = { - "message": "文件已上传并排队等待处理", - "task_id": task_id, - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{task_type}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - } - logging.info(f"上传文件完成: {task_id}") - return JSONResponse(content=response_data) - -# 为每个任务类型创建单独的端点 -@v1_app.post("/pose") -async def upload_pose(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - logging.info(f"收到 /pose端点的请求") - return await upload_file(file, task_type="pose", api_key_info=api_key_info) - -@v1_app.post("/cpm") -async def upload_cpm(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="cpm", api_key_info=api_key_info) - -@v1_app.post("/qwenvl") -async def upload_qwenvl(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="qwenvl", api_key_info=api_key_info) - -@v1_app.post("/yolo") -async def upload_yolo(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="yolo", api_key_info=api_key_info) - -@v1_app.post("/fall") -async def upload_fall(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="fall", api_key_info=api_key_info) - -@v1_app.post("/face") -async def upload_face(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - logging.info(f"收到 /face 端点的请求") - return await upload_file(file, task_type="face", api_key_info=api_key_info) - -@v1_app.post("/mediapipe") -async def upload_mediapipe(file: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - return await upload_file(file, task_type="mediapipe", api_key_info=api_key_info) - - -@v1_app.get("/result/{task_id}") -async def get_result(task_id: str, api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - # 从 REDIS_DB (15) 获取任务状态 - task_info = redis_client.hgetall(f"task:{task_id}") - if not task_info: - raise HTTPException(status_code=404, detail="Task not found") - - task_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in task_info.items()} - - if task_info['status'] != 'completed': - return {"status": task_info['status'], "message": "Task is not completed yet"} - - result_type = task_info['result_type'] - result_key = task_info['result_key'] - - # 根据任务类型选择相应的 Redis 客户端 - redis_client_map = { - 'pose': redis_pose_client, - 'cpm': redis_cpm_client, - 'yolo': redis_yolo_client, - 'face': redis_face_client, - 'fall': redis_fall_client, - 'mediapipe': redis_mediapipe_client, - 'qwenvl': redis_qwenvl_client - } - - result_redis = redis_client_map.get(result_type) - if not result_redis: - raise HTTPException(status_code=400, detail="Unsupported result type") - - result = result_redis.hgetall(result_key) - if not result: - raise HTTPException(status_code=404, detail=f"{result_type.upper()} result not found") - - result = {k.decode('utf-8'): v.decode('utf-8') for k, v in result.items()} - - # 将 result 字段解析为 JSON(如果存在) - if 'result' in result: - result['result'] = json.loads(result['result']) - - return { - "status": "completed", - "result_type": result_type, - "result": result - } - -@v1_app.get("/annotated/{task_id}") -async def get_annotated_image(task_id: str, api_key: str = Depends(get_api_key)): - api_key_info = await verify_api_key(api_key) - if not api_key_info: - raise HTTPException(status_code=403, detail="无效的API密钥") - - # 从 REDIS_DB (15) 获取任务信息 - task_info = redis_client.hgetall(f"task:{task_id}") - if not task_info: - raise HTTPException(status_code=404, detail="Task not found") - - task_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in task_info.items()} - - if task_info['status'] != 'completed': - raise HTTPException(status_code=400, detail="Task is not completed yet") - - result_type = task_info.get('result_type') - result_key = task_info.get('result_key') - - if not result_key: - raise HTTPException(status_code=404, detail="Result key not found") - - if result_type in ['cpm', 'qwenvl']: - raise HTTPException(status_code=400, detail="Annotated image not available for this task type") - - # 根据任务类型选择相应的 Redis 客户端 - redis_client_map = { - 'pose': redis_pose_client, - 'yolo': redis_yolo_client, - 'face': redis_face_client, - 'fall': redis_fall_client, - 'mediapipe': redis_mediapipe_client - } - - result_redis = redis_client_map.get(result_type) - if not result_redis: - raise HTTPException(status_code=400, detail="Unsupported result type") - - result = result_redis.hgetall(result_key) - if not result: - raise HTTPException(status_code=404, detail=f"{result_type.upper()} result not found") - - result = {k.decode('utf-8'): v.decode('utf-8') for k, v in result.items()} - - result_file = result.get('result_file') - if not result_file: - raise HTTPException(status_code=404, detail="Result file not found") - - file_path = os.path.join(RESULT_DIR, result_file) - if not os.path.exists(file_path): - raise HTTPException(status_code=404, detail="Result image file not found") - - return FileResponse(file_path, media_type="image/png") - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8005) \ No newline at end of file diff --git a/chat_history/ChatTTS/.gitkeep b/chat_history/ChatTTS/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/chat_history/chat_ser2.py b/chat_history/chat_ser2.py deleted file mode 100644 index b510914..0000000 --- a/chat_history/chat_ser2.py +++ /dev/null @@ -1,88 +0,0 @@ -from flask import Flask, request, send_file, jsonify -import ChatTTS -import tempfile -import numpy as np -import soundfile as sf -from flask_cors import CORS -import os -import pickle -import asyncio -from concurrent.futures import ThreadPoolExecutor -import time -import hashlib - -app = Flask(__name__) -CORS(app) - -chat_tts = ChatTTS.Chat() -chat_tts.load(compile=False) - -SAMPLE_RATE = 24000 - -SPEAKER_EMBEDDING_FILE = 'cutegirl_speaker_embedding.pkl' -AUDIO_DIR = '/www/wwwroot/chat.obscura.work/audio_files' - -with open(SPEAKER_EMBEDDING_FILE, 'rb') as f: - FIXED_SPEAKER = pickle.load(f) - -executor = ThreadPoolExecutor(max_workers=3) - -def generate_audio(text): - params_infer_code = ChatTTS.Chat.InferCodeParams( - spk_emb=FIXED_SPEAKER, - temperature=0.3, - top_P=0.6, - top_K=20, - ) - - wavs = chat_tts.infer(text, params_infer_code=params_infer_code) - audio_data = wavs[0] - - if not np.issubdtype(audio_data.dtype, np.floating): - audio_data = audio_data.astype(np.float32) - - if np.max(np.abs(audio_data)) > 1: - audio_data = audio_data / np.max(np.abs(audio_data)) - - return audio_data - -def get_audio_filename(text): - return hashlib.md5(text.encode()).hexdigest() + '.wav' - -@app.route('/synthesize', methods=['POST', 'OPTIONS']) -async def synthesize(): - if request.method == 'OPTIONS': - return '', 204 - - data = request.json - texts = data.get('texts') - if not texts: - return jsonify({"error": "No texts provided"}), 400 - - audio_urls = [] - - for text in texts: - filename = get_audio_filename(text) - filepath = os.path.join(AUDIO_DIR, filename) - - if os.path.exists(filepath): - audio_urls.append(f"/audio_files/{filename}") - else: - loop = asyncio.get_event_loop() - audio_data = await loop.run_in_executor(executor, generate_audio, text) - sf.write(filepath, audio_data, SAMPLE_RATE) - audio_urls.append(f"/audio_files/{filename}") - - return jsonify({"audio_urls": audio_urls}) - -@app.route('/audio_files/', methods=['GET', 'OPTIONS']) -def get_audio(filename): - if request.method == 'OPTIONS': - return '', 204 - try: - return send_file(os.path.join(AUDIO_DIR, filename), mimetype='audio/wav') - except Exception as e: - return jsonify({"error": str(e)}), 404 - -if __name__ == '__main__': - app.run(port=5002) \ No newline at end of file diff --git a/chat_history/chattts_service.py b/chat_history/chattts_service.py deleted file mode 100644 index cc2a7b5..0000000 --- a/chat_history/chattts_service.py +++ /dev/null @@ -1,92 +0,0 @@ -from flask import Flask, request, send_file, jsonify -import ChatTTS -import tempfile -import numpy as np -import soundfile as sf -from flask_cors import CORS -import os -import random -import torch -import pickle - - -app = Flask(__name__) -CORS(app) - -# 初始化 ChatTTS -chat_tts = ChatTTS.Chat() -chat_tts.load(compile=False) - -# 定义采样率 -SAMPLE_RATE = 24000 - -# # 生成一个固定的说话人嵌入 -# FIXED_SPEAKER = chat_tts.sample_random_speaker() - -# 文件名用于保存和加载说话人嵌入 -SPEAKER_EMBEDDING_FILE = 'two_speaker_embedding.pkl' - -def get_or_create_fixed_speaker(): - try: - if os.path.exists(SPEAKER_EMBEDDING_FILE): - with open(SPEAKER_EMBEDDING_FILE, 'rb') as f: - fixed_speaker = pickle.load(f) - else: - fixed_speaker = chat_tts.sample_random_speaker() - with open(SPEAKER_EMBEDDING_FILE, 'wb') as f: - pickle.dump(fixed_speaker, f) - except (EOFError, pickle.UnpicklingError): - print("Warning: Unable to load speaker embedding. Creating a new one.") - fixed_speaker = chat_tts.sample_random_speaker() - with open(SPEAKER_EMBEDDING_FILE, 'wb') as f: - pickle.dump(fixed_speaker, f) - return fixed_speaker - - -# 获取或创建固定的说话人嵌入 -FIXED_SPEAKER = get_or_create_fixed_speaker() - -@app.route('/synthesize', methods=['POST']) -def synthesize(): - data = request.json - text = data.get('text') - if not text: - return jsonify({"error": "No text provided"}), 400 - - temp_file = None - try: - params_infer_code = ChatTTS.Chat.InferCodeParams( - spk_emb=FIXED_SPEAKER, - temperature=0.3, - top_P=0.7, - top_K=20, - ) - - params_refine_text = ChatTTS.Chat.RefineTextParams( - prompt='[oral_2][laugh_0][break_6]', - ) - - wavs = chat_tts.infer(text, params_refine_text=params_refine_text, params_infer_code=params_infer_code) - - audio_data = wavs[0] - - if not np.issubdtype(audio_data.dtype, np.floating): - audio_data = audio_data.astype(np.float32) - - if np.max(np.abs(audio_data)) > 1: - audio_data = audio_data / np.max(np.abs(audio_data)) - - temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav") - sf.write(temp_file.name, audio_data, SAMPLE_RATE) - - return send_file(temp_file.name, mimetype='audio/wav') - - except Exception as e: - return jsonify({"error": str(e)}), 500 - - finally: - if temp_file and os.path.exists(temp_file.name): - os.unlink(temp_file.name) - -if __name__ == '__main__': - app.run(port=5002) \ No newline at end of file diff --git a/chat_history/kafka_chat/demo.py b/chat_history/kafka_chat/demo.py deleted file mode 100644 index 7e930db..0000000 --- a/chat_history/kafka_chat/demo.py +++ /dev/null @@ -1,49 +0,0 @@ -import argparse -import os -import soundfile as sf - -from tools.i18n.i18n import I18nAuto -from GPT_SoVITS.inference_webui import change_gpt_weights, change_sovits_weights, get_tts_wav - -i18n = I18nAuto() - -def synthesize(GPT_model_path, SoVITS_model_path, ref_audio_path, ref_text_path, ref_language, target_text, target_language, output_path, output_filename): - # Read reference text - with open(ref_text_path, 'r', encoding='utf-8') as file: - ref_text = file.read() - - # Change model weights - change_gpt_weights(gpt_path=GPT_model_path) - change_sovits_weights(sovits_path=SoVITS_model_path) - - # Synthesize audio - synthesis_result = get_tts_wav(ref_wav_path=ref_audio_path, - prompt_text=ref_text, - prompt_language=i18n(ref_language), - text=target_text, - text_language=i18n(target_language), top_p=1, temperature=1) - - result_list = list(synthesis_result) - - if result_list: - last_sampling_rate, last_audio_data = result_list[-1] - output_wav_path = os.path.join(output_path, output_filename) - sf.write(output_wav_path, last_audio_data, last_sampling_rate) - print(f"Audio saved to {output_wav_path}") - -def main(): - GPT_model_path = "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s1bert25hz-5kh-longer-epoch=12-step=369668.ckpt" - SoVITS_model_path = "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s2G2333k.pth" - ref_audio_path = "/home/zydi//worker_chat/kafka/sample/woman.wav" - ref_text_path = "/home/zydi//worker_chat/kafka/sample/woman.txt" - ref_language = "中文" - target_text = """我们开发了"病人实时健康监测系统"和"AI辅助诊断系统",这些系统显著提高了医疗诊断的效率和准确性。obscura形成了全面的医疗智能解决方案""" - - target_language = "多语种混合" - output_path = "/home/zydi//worker_chat/kafka" - output_filename = "output.wav" - - synthesize(GPT_model_path, SoVITS_model_path, ref_audio_path, ref_text_path, ref_language, target_text, target_language, output_path, output_filename) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/chat_history/kafka_chat/text_input_db.py b/chat_history/kafka_chat/text_input_db.py deleted file mode 100644 index e0c9ccd..0000000 --- a/chat_history/kafka_chat/text_input_db.py +++ /dev/null @@ -1,210 +0,0 @@ -import json -import threading -import redis -from kafka import KafkaConsumer, KafkaProducer -from pymongo import MongoClient,TEXT -import requests - -DEFAULT_SYSTEM_PROMPT = "你是一个智能助手,请用尽可能简短、简洁的方式回答问题。当用户询问数据库相关内容时,你可以访问MongoDB数据库来获取额外的信息。" - -def search_mongodb(mongodb_client, query, mongo_db, search_collection): - collection = mongodb_client[mongo_db][search_collection] - results = collection.find({"$text": {"$search": query}}) - - processed_results = [] - for result in results: - if 'original_answer' in result: - processed_results.append(result['original_answer']) - else: - # 记录这个问题并跳过这个结果 - print(f"警告: 文档缺少 'original_answer' 字段: {result['_id']}") - - return processed_results - -def get_conversation_history(redis_client, conversation_id, max_history=5): - history = redis_client.lrange(f"conversation:{conversation_id}", 0, max_history * 2 - 1) - return list(zip(history[::2], history[1::2])) - -def add_to_conversation_history(redis_client, conversation_id, query, answer): - redis_client.rpush(f"conversation:{conversation_id}", query, answer) - redis_client.expire(f"conversation:{conversation_id}", 3600) # 设置1小时的过期时间 - -def generate_answer(query, context, history, mongo_client, mongo_db, search_collection): - full_prompt = DEFAULT_SYSTEM_PROMPT + "\n" - for past_query, past_response in history: - full_prompt += f"用户: {past_query}\n助手: {past_response}\n" - full_prompt += f"用户: {query}\n上下文: {context}\n\n" - full_prompt += "请根据上下文和历史对话回答用户的问题。如果需要额外信息,你可以使用以下函数查询MongoDB数据库:\n" - full_prompt += "search_mongodb(query: str) -> List[str]\n" - full_prompt += "该函数会返回与查询相关的内容列表。\n" - full_prompt += "助手: " - - def search_mongodb_wrapper(query): - return search_mongodb(mongo_client, query, mongo_db, search_collection) - - data = { - "model": "llama3.1", - "prompt": full_prompt, - "stream": True, - "temperature": 0, - "functions": [ - { - "name": "search_mongodb", - "description": "Search for information in MongoDB", - "parameters": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The search query" - } - }, - "required": ["query"] - } - } - ], - "function_call": "auto" - } - - try: - response = requests.post("http://127.0.0.1:11434/api/generate", json=data, stream=True) - response.raise_for_status() - - text_output = "" - for line in response.iter_lines(): - if line: - json_data = json.loads(line) - if 'response' in json_data: - text_output += json_data['response'] - elif 'function_call' in json_data: - function_call = json.loads(json_data['function_call']) - if function_call['name'] == 'search_mongodb': - search_results = search_mongodb_wrapper(function_call['arguments']['query']) - text_output += f"根据MongoDB查询结果:{', '.join(search_results)}\n" - - return text_output - - except Exception as e: - print(f"Error generating answer: {str(e)}") - return "抱歉,生成回答时出现错误。" - -def process_message(message, kafka_config, mongodb_client, redis_client, mongo_db, search_collection): - try: - # 检查 message.value 是否已经是字典 - if isinstance(message.value, dict): - message_data = message.value - else: - # 如果不是字典,尝试解析为 JSON - message_data = json.loads(message.value) - - query = message_data.get('text', '') - conversation_id = message_data.get('conversation_id', 'default') - except (json.JSONDecodeError, AttributeError): - # 如果解析失败或 message.value 不是预期的格式,假设整个消息就是查询文本 - query = str(message.value) - conversation_id = 'default' - - # 获取对话历史 - history = get_conversation_history(redis_client, conversation_id) - - # 在 MongoDB 中搜索相关信息 - search_results = search_mongodb(mongodb_client, query, mongo_db, search_collection) - context = " ".join(search_results) - - # 使用 llama3.1:8b 模型生成答案 - answer = generate_answer(query, context, history, mongodb_client, mongo_db, search_collection) - - # 将对话添加到 Redis 历史记录 - add_to_conversation_history(redis_client, conversation_id, query, answer) - - # 将答案发送到 Kafka 的 voice-output 主题 - producer = KafkaProducer(bootstrap_servers=[kafka_config['bootstrap_servers']], - value_serializer=lambda x: json.dumps(x).encode('utf-8')) - producer.send(kafka_config['voice_output_topic'], {'answer': answer, 'conversation_id': conversation_id}) - - print(f"Processed message: {query}") - print(f"Generated answer: {answer}") - print(f"Sent to voice-output topic: {{'answer': '{answer}', 'conversation_id': '{conversation_id}'}}") -def consumer_thread(kafka_config, mongodb_config, redis_config, consumer_group, thread_id): - consumer = KafkaConsumer( - kafka_config['text_input_topic'], - bootstrap_servers=[kafka_config['bootstrap_servers']], - group_id=consumer_group, - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - auto_offset_reset='earliest', - enable_auto_commit=True - ) - - mongodb_client = MongoClient(mongodb_config['uri']) - redis_client = redis.Redis( - host=redis_config['host'], - port=redis_config['port'], - db=redis_config['db'], - password=redis_config['password'] - ) - - print(f"Consumer thread {thread_id} started, listening to {kafka_config['text_input_topic']} topic, consumer group: {consumer_group}") - - for message in consumer: - print(f"Thread {thread_id} received message in partition {message.partition}, offset: {message.offset}") - process_message(message, kafka_config, mongodb_client, redis_client, - mongodb_config['db_name'], mongodb_config['search_collection']) - -def main(kafka_config, mongodb_config, redis_config): - threads = [] - consumer_group = f"{kafka_config['consumer_group_prefix']}_single" - - for i in range(kafka_config['num_threads']): - thread = threading.Thread( - target=consumer_thread, - args=(kafka_config, mongodb_config, redis_config, consumer_group, i) - ) - thread.start() - threads.append(thread) - print(f"Started consumer thread {i}, consumer group: {consumer_group}") - - # Wait for all threads to complete (in reality, they will run indefinitely) - for thread in threads: - thread.join() - -if __name__ == "__main__": - # Kafka configuration - KAFKA_BOOTSTRAP_SERVERS = '222.186.136.78:9092' - KAFKA_INPUT_TOPIC = 'text-input' - KAFKA_OUTPUT_TOPIC = 'voice-output' - KAFKA_CONSUMER_GROUP_PREFIX = 'text_group' - KAFKA_NUM_THREADS = 3 # 您可以根据需要调整线程数 - - # MongoDB configuration - MONGO_URI = 'mongodb://minio_mongo:BCd4npzKBnwmCRdh@222.186.136.78:27017/?authSource=minio_mongo' - MONGO_DB = 'minio_mongo' - MONGO_SEARCH_COLLECTION = 'cpm' - - # Redis configuration - REDIS_HOST = '222.186.136.78' - REDIS_PORT = 6379 - REDIS_DB = 0 - REDIS_PASSWORD = 'Obscura@2024' # 添加Redis密码 - - kafka_config = { - 'bootstrap_servers': KAFKA_BOOTSTRAP_SERVERS, - 'text_input_topic': KAFKA_INPUT_TOPIC, - 'voice_output_topic': KAFKA_OUTPUT_TOPIC, - 'consumer_group_prefix': KAFKA_CONSUMER_GROUP_PREFIX, - 'num_threads': KAFKA_NUM_THREADS - } - - mongodb_config = { - 'uri': MONGO_URI, - 'db_name': MONGO_DB, - 'search_collection': MONGO_SEARCH_COLLECTION - } - - redis_config = { - 'host': REDIS_HOST, - 'port': REDIS_PORT, - 'db': REDIS_DB, - 'password': REDIS_PASSWORD # 添加Redis密码 - } - - main(kafka_config, mongodb_config, redis_config) \ No newline at end of file diff --git a/chat_history/kafka_chat/voice_input.py b/chat_history/kafka_chat/voice_input.py deleted file mode 100644 index 5ecc4ca..0000000 --- a/chat_history/kafka_chat/voice_input.py +++ /dev/null @@ -1,169 +0,0 @@ -import os -import json -import uuid -from kafka import KafkaConsumer, KafkaProducer, TopicPartition -import whisper -from pydub import AudioSegment -import io -import tempfile -from minio import Minio -import threading -import requests - -def get_audio_from_minio(minio_client, bucket, object_name): - try: - response = minio_client.get_object(bucket, object_name) - return response.read() - except Exception as e: - print(f"从 MinIO 获取音频时出错: {str(e)}") - print(f"Bucket: {bucket}, Object: {object_name}") - return None - -def process_audio(model, audio_data, file_extension): - with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{file_extension}') as temp_audio_file: - temp_audio_file.write(audio_data) - temp_audio_file.flush() - - if file_extension.lower() != 'wav': - audio = AudioSegment.from_file(temp_audio_file.name, format=file_extension) - wav_path = temp_audio_file.name + '.wav' - audio.export(wav_path, format="wav") - else: - wav_path = temp_audio_file.name - - result = model.transcribe(wav_path) - - os.unlink(temp_audio_file.name) - if file_extension.lower() != 'wav': - os.unlink(wav_path) - - return json.dumps({"text": result["text"]}) - -def consumer_thread(kafka_config, minio_config, model, partition): - client_id = f'voice_{partition}' - group_id = f"{kafka_config['consumer_group']}_{partition}" - - consumer = KafkaConsumer( - bootstrap_servers=[kafka_config['bootstrap_servers']], - group_id=group_id, - client_id=client_id, - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - enable_auto_commit=True, - auto_commit_interval_ms=5000 - ) - - # 手动分配分区 - topic_partition = TopicPartition(kafka_config['voice_input_topic'], partition) - consumer.assign([topic_partition]) - - producer = KafkaProducer( - bootstrap_servers=[kafka_config['bootstrap_servers']], - value_serializer=lambda x: json.dumps(x).encode('utf-8') - ) - - minio_client = Minio( - minio_config['endpoint'], - access_key=minio_config['access_key'], - secret_key=minio_config['secret_key'], - secure=minio_config['secure'] - ) - - print(f"消费者 {client_id} 开始监听 {kafka_config['voice_input_topic']} 主题的分区 {partition}...") - - - for message in consumer: - print(f"消费者 {client_id} 从分区 {partition} 收到新的音频消息") - event_info = message.value - print(f"事件信息: {event_info}") - - # 从S3事件中提取音频文件路径 - audio_path = event_info.get('Key') - if not audio_path: - # 如果在顶层没有找到Key,尝试从Records中获取 - records = event_info.get('Records', []) - if records: - audio_path = records[0].get('s3', {}).get('object', {}).get('key') - - if not audio_path: - print(f"消费者 {client_id} 无法从事件中获取音频路径") - continue - - # 移除可能的 'audio/' 前缀 - if audio_path.startswith('audio/'): - audio_path = audio_path[6:] - - file_extension = audio_path.split('.')[-1] if '.' in audio_path else 'wav' - - print(f"尝试从MinIO获取音频: {audio_path}") - audio_data = get_audio_from_minio(minio_client, minio_config['bucket'], audio_path) - - if audio_data: - try: - transcribed_result = process_audio(model, audio_data, file_extension) - transcribed_data = json.loads(transcribed_result) - transcribed_text = transcribed_data["text"] - print(f"消费者 {client_id} 识别结果: {transcribed_text}") - - audio_info = { - 'file_name': audio_path, - 'file_extension': file_extension, - 'size': len(audio_data) - } - - # 发送到下一个Kafka主题 - producer.send(kafka_config['text_input_topic'], value=json.dumps({ - 'text': transcribed_text, - 'audio_info': audio_info, - 'consumer_id': client_id, - 'partition': partition - })) - print(f"消费者 {client_id} 已发送识别结果到 {kafka_config['text_input_topic']} 主题") - - # # 发送结果到PHP服务 - # send_to_php(transcribed_text, audio_info) - - except Exception as e: - print(f"消费者 {client_id} 处理音频时出错: {str(e)}") - import traceback - traceback.print_exc() - else: - print(f"消费者 {client_id} 无法获取音频数据") - - consumer.close() - -def main(kafka_config, minio_config, whisper_config): - model = whisper.load_model(whisper_config['model_name']) - - threads = [] - for partition in range(kafka_config['num_partitions']): - thread = threading.Thread(target=consumer_thread, args=(kafka_config, minio_config, model, partition)) - thread.start() - threads.append(thread) - - for thread in threads: - thread.join() - -if __name__ == "__main__": - # Kafka 配置 - kafka_config = { - 'bootstrap_servers': '222.186.136.78:9092', - 'voice_input_topic': 'voice-input', - 'text_input_topic': 'text-input', - 'consumer_group': 'voice_group', - 'num_partitions': 3 # 修改为实际的分区数 - } - - # MinIO 配置 - minio_config = { - 'endpoint': "api.obscura.work", - 'access_key': "00v3MtLtIAIkR3hkIuYR", - 'secret_key': "XfDeVe5bJjPU21NEYc023gzJVUTJzQqxsWHqIKMf", - 'bucket': 'audio', - 'secure': True - } - # Whisper 配置 - whisper_config = { - 'model_name': 'large-v3' # 可以根据需要选择不同的模型大小 - } - - main(kafka_config, minio_config, whisper_config) \ No newline at end of file diff --git a/chat_history/kafka_chat/voice_output.py b/chat_history/kafka_chat/voice_output.py deleted file mode 100644 index 231fb73..0000000 --- a/chat_history/kafka_chat/voice_output.py +++ /dev/null @@ -1,202 +0,0 @@ -import json -import hashlib -import io -import threading -from concurrent.futures import ThreadPoolExecutor -from kafka import KafkaConsumer, KafkaProducer -from minio import Minio -from GPT_SoVITS.inference_webui import get_tts_wav -from tools.i18n.i18n import I18nAuto -import soundfile as sf -import redis - -i18n = I18nAuto() - -# Global variables -global_model_config = None - -def initialize_models(model_config): - global global_model_config - global_model_config = model_config - print("Models initialized") - -def generate_content_hash(text): - return hashlib.md5(text.encode()).hexdigest() - -def synthesize(target_text): - global global_model_config - - with open(global_model_config['ref_text_path'], 'r', encoding='utf-8') as file: - ref_text = file.read() - - synthesis_result = get_tts_wav( - ref_wav_path=global_model_config['ref_audio_path'], - prompt_text=ref_text, - prompt_language=i18n(global_model_config['ref_language']), - text=target_text, - text_language=i18n(global_model_config['target_language']), - top_p=1, temperature=1 - ) - - result_list = list(synthesis_result) - print(f"Synthesizing audio for text: {target_text}") - if result_list: - last_sampling_rate, last_audio_data = result_list[-1] - return last_sampling_rate, last_audio_data - return None, None - -def process_message(message_data, minio_client, minio_config, redis_client): - target_text = message_data.get('answer', message_data.get('text', '')) - content_hash = generate_content_hash(target_text) - - # Check Redis cache - cached_audio_info = redis_client.get(content_hash) - if cached_audio_info: - print(f"Using Redis cached audio, content hash: {content_hash}") - return json.loads(cached_audio_info) - - # If not in Redis, check MinIO - bucket_name = minio_config['bucket'] - object_name = f"{content_hash}.wav" - try: - minio_client.stat_object(bucket_name, object_name) - print(f"Using existing audio from MinIO, content hash: {content_hash}") - audio_info = { - 'id': content_hash, - 'text': target_text, - 'content_hash': content_hash, - 'minio_bucket': bucket_name, - 'minio_object': object_name, - 'status': 'completed', - } - redis_client.set(content_hash, json.dumps(audio_info)) - return audio_info - except: - pass # Object doesn't exist in MinIO, continue to synthesis - - # If not found, synthesize new audio - try: - sampling_rate, audio_data = synthesize(target_text) - - if audio_data is not None: - audio_id = content_hash - object_name = f"{audio_id}.wav" - audio_buffer = io.BytesIO() - sf.write(audio_buffer, audio_data, sampling_rate, format='wav') - audio_buffer.seek(0) - - minio_client.put_object( - bucket_name, object_name, audio_buffer, - length=audio_buffer.getbuffer().nbytes - ) - - etag = minio_client.stat_object(bucket_name, object_name).etag - - audio_info = { - 'id': audio_id, - 'text': target_text, - 'sampling_rate': sampling_rate, - 'content_hash': content_hash, - 'minio_bucket': bucket_name, - 'minio_object': object_name, - 'etag': etag, - 'status': 'completed', - } - - redis_client.set(content_hash, json.dumps(audio_info)) - - return audio_info - except Exception as e: - print(f"Error processing message: {e}") - error_info = { - 'status': 'failed', - 'error': str(e), - 'text': target_text, - 'content_hash': content_hash, - } - redis_client.set(content_hash, json.dumps(error_info)) - return None - -def message_handler(message, minio_client, minio_config, redis_client): - print(f"Processing message: {message.value}") - message_data = json.loads(message.value) - audio_info = process_message(message_data, minio_client, minio_config, redis_client) - -def consumer_thread(consumer_id, kafka_config, minio_config, redis_config): - consumer = KafkaConsumer( - kafka_config['text_input_topic'], - bootstrap_servers=kafka_config['bootstrap_servers'], - auto_offset_reset='latest', - enable_auto_commit=True, - group_id=kafka_config['consumer_group'] - ) - - minio_client = Minio( - minio_config['endpoint'], - access_key=minio_config['access_key'], - secret_key=minio_config['secret_key'], - secure=minio_config['secure'] - ) - - redis_client = redis.Redis( - host=redis_config['host'], - port=redis_config['port'], - db=redis_config['db'], - password=redis_config['password'] - ) - - with ThreadPoolExecutor(max_workers=kafka_config['threads_per_consumer']) as executor: - print(f"Consumer {consumer_id} started running") - for message in consumer: - executor.submit(message_handler, message, minio_client, minio_config, redis_client) - -def main(kafka_config, minio_config, model_config, redis_config): - initialize_models(model_config) - - threads = [] - for i in range(kafka_config['num_consumers']): - t = threading.Thread(target=consumer_thread, args=(i, kafka_config, minio_config, redis_config)) - threads.append(t) - t.start() - - for t in threads: - t.join() - -if __name__ == "__main__": - # Kafka configuration - kafka_config = { - 'bootstrap_servers': '222.186.136.78:9092', - 'text_input_topic': 'voice-output', - 'consumer_group': 'voice_group', - 'num_consumers': 3, - 'threads_per_consumer': 4 - } - - # MinIO configuration - minio_config = { - 'endpoint': "api.obscura.work", - 'access_key': "00v3MtLtIAIkR3hkIuYR", - 'secret_key': "XfDeVe5bJjPU21NEYc023gzJVUTJzQqxsWHqIKMf", - 'bucket': 'tts-audio', - 'secure': True - } - - # Redis configuration - redis_config = { - 'host': '222.186.136.78', - 'port': 6379, - 'db': 4, - 'password': "Obscura@2024" - } - - # Model configuration - model_config = { - 'GPT_model_path': "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s1bert25hz-5kh-longer-epoch=12-step=369668.ckpt", - 'SoVITS_model_path': "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s2G2333k.pth", - 'ref_audio_path': "sample/woman.wav", - 'ref_text_path': "sample/woman.txt", - 'ref_language': "中文", - 'target_language': "多语种混合" - } - - main(kafka_config, minio_config, model_config, redis_config) \ No newline at end of file diff --git a/chat_history/kafka_chat/weight.json b/chat_history/kafka_chat/weight.json deleted file mode 100644 index 58ec624..0000000 --- a/chat_history/kafka_chat/weight.json +++ /dev/null @@ -1 +0,0 @@ -{"GPT": {"v2": "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s1bert25hz-5kh-longer-epoch=12-step=369668.ckpt"}, "SoVITS": {"v2": "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s2G2333k.pth"}} \ No newline at end of file diff --git a/chat_history/producer_chat.py b/chat_history/producer_chat.py deleted file mode 100644 index 75a70d2..0000000 --- a/chat_history/producer_chat.py +++ /dev/null @@ -1,406 +0,0 @@ -from fastapi import FastAPI, HTTPException, Depends, Security, File, UploadFile -from fastapi.middleware.cors import CORSMiddleware -from fastapi.security import APIKeyHeader -from fastapi.responses import FileResponse -from pydantic import BaseModel -from kafka import KafkaProducer -from redis import Redis -import os -import json -import uuid -from datetime import datetime, timezone -from dotenv import load_dotenv -import tempfile -import hashlib -from pydantic import BaseModel, Field - -# 在文件顶部添加这个函数 -def get_audio_hash(text): - return hashlib.md5(text.encode()).hexdigest() - -# 加载 .env 文件 -load_dotenv() - -app = FastAPI() -v1_chat_app = FastAPI() -app.mount("/v1_chat", v1_chat_app) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 配置 -KAFKA_BROKER = os.getenv('KAFKA_BROKER') -REDIS_HOST = os.getenv('REDIS_HOST') -REDIS_PORT = int(os.getenv('REDIS_PORT')) -REDIS_PASSWORD = os.getenv('REDIS_PASSWORD') -REDIS_TTS_DB = int(os.getenv('REDIS_TTS_DB')) -REDIS_ASR_DB = int(os.getenv('REDIS_ASR_DB')) -REDIS_CHAT_DB = int(os.getenv('REDIS_CHAT_DB')) -REDIS_API_DB = int(os.getenv('REDIS_API_DB')) -REDIS_API_USAGE_DB = int(os.getenv('REDIS_API_USAGE_DB')) -REDIS_TASK_DB = int(os.getenv('REDIS_TASK_DB')) - - -# Redis 配置 -REDIS_GIRL_DB = int(os.getenv('REDIS_GIRL_DB')) -REDIS_WOMAN_DB = int(os.getenv('REDIS_WOMAN_DB')) -REDIS_MAN_DB = int(os.getenv('REDIS_MAN_DB')) -REDIS_LEIJUN_DB = int(os.getenv('REDIS_LEIJUN_DB')) -REDIS_DUFU_DB = int(os.getenv('REDIS_DUFU_DB')) -REDIS_HEJIONG_DB = int(os.getenv('REDIS_HEJIONG_DB')) -REDIS_MAHUATENG_DB = int(os.getenv('REDIS_MAHUATENG_DB')) -REDIS_LIDAN_DB = int(os.getenv('REDIS_LIDAN_DB')) -REDIS_YUHUA_DB = int(os.getenv('REDIS_YUHUA_DB')) -REDIS_LIUZHENYUN_DB = int(os.getenv('REDIS_LIUZHENYUN_DB')) -REDIS_DABING_DB = int(os.getenv('REDIS_DABING_DB')) -REDIS_LUOXIANG_DB = int(os.getenv('REDIS_LUOXIANG_DB')) -REDIS_XUZHIYUAN_DB = int(os.getenv('REDIS_XUZHIYUAN_DB')) - -KAFKA_TTS_TOPIC = os.getenv('KAFKA_TTS_TOPIC') -KAFKA_ASR_TOPIC = os.getenv('KAFKA_ASR_TOPIC') -KAFKA_CHAT_TOPIC = os.getenv('KAFKA_CHAT_TOPIC') - -OUTPUT_PATH= os.getenv('OUTPUT_PATH') - -# 初始化 Kafka Producer -producer = KafkaProducer( - bootstrap_servers=[KAFKA_BROKER], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# 初始化 Redis -redis_tts_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_TTS_DB) -redis_asr_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_ASR_DB) -redis_chat_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_CHAT_DB) -redis_api_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_API_DB) -redis_api_usage_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_API_USAGE_DB) -redis_task_client = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_TASK_DB) - -redis_tts_girl = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_GIRL_DB) -redis_tts_woman = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_WOMAN_DB) -redis_tts_man = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_MAN_DB) -redis_tts_leijun = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_LEIJUN_DB) -redis_tts_dufu = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_DUFU_DB) -redis_tts_hejiong = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_HEJIONG_DB) -redis_tts_mahuateng = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_MAHUATENG_DB) -redis_tts_lidan = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_LIDAN_DB) -redis_tts_yuhua = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_YUHUA_DB) -redis_tts_liuzhenyun = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_LIUZHENYUN_DB) -redis_tts_dabing = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_DABING_DB) -redis_tts_luoxiang = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_LUOXIANG_DB) -redis_tts_xuzhiyuan = Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=REDIS_XUZHIYUAN_DB) - -# 创建一个音色到对应 Redis 客户端的映射 -voice_to_redis = { - "default": redis_tts_girl, - "girl": redis_tts_girl, - "woman": redis_tts_woman, - "man": redis_tts_man, - "leijun": redis_tts_leijun, - "dufu": redis_tts_dufu, - "hejiong": redis_tts_hejiong, - "mahuateng": redis_tts_mahuateng, - "lidan": redis_tts_lidan, - "yuhua": redis_tts_yuhua, - "liuzhenyun": redis_tts_liuzhenyun, - "dabing": redis_tts_dabing, - "luoxiang": redis_tts_luoxiang, - "xuzhiyuan": redis_tts_xuzhiyuan -} - - -# 定义API密钥头部 -api_key_header = APIKeyHeader(name="Authorization", auto_error=False) - -def get_audio_hash(text): - return hashlib.md5(text.encode()).hexdigest() - -# 验证API密钥的函数 -async def get_api_key(api_key: str = Security(api_key_header)): - if api_key and api_key.startswith("Bearer "): - key = api_key.split(" ")[1] - if key.startswith("obs-"): - return key - raise HTTPException( - status_code=401, - detail="无效的API密钥", - headers={"WWW-Authenticate": "Bearer"}, - ) - -async def verify_api_key(api_key: str = Depends(get_api_key)): - redis_key = f"api_key:{api_key}" - - api_key_info = redis_api_client.hgetall(redis_key) - - if not api_key_info: - raise HTTPException(status_code=401, detail="无效的API密钥") - - api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()} - - if api_key_info.get('is_active') != '1': - raise HTTPException(status_code=401, detail="API密钥已停用") - - expires_at = datetime.fromisoformat(api_key_info.get('expires_at')) - if datetime.now(timezone.utc) > expires_at: - raise HTTPException(status_code=401, detail="API密钥已过期") - - usage_info = redis_api_usage_client.hgetall(redis_key) - usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()} - - return { - "api_key": api_key, - **api_key_info, - **usage_info - } - -async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str): - redis_key = f"api_key:{api_key}" - current_time = datetime.now(timezone.utc).isoformat() - - pipe = redis_api_usage_client.pipeline() - - pipe.hincrby(redis_key, "tokens_used", new_tokens_used) - pipe.hset(redis_key, "last_used_at", current_time) - - model_tokens_field = f"{model_name}_tokens_used" - model_last_used_field = f"{model_name}_last_used_at" - - pipe.hincrby(redis_key, model_tokens_field, new_tokens_used) - pipe.hset(redis_key, model_last_used_field, current_time) - - pipe.execute() - -async def process_request(api_key_info: dict, model_name: str, tokens_required: int, task_data: dict, kafka_topic: str): - api_key = api_key_info['api_key'] - usage_key = f"api_key:{api_key}" - total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0) - tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0) - - if tokens_used + tokens_required > total_tokens: - raise HTTPException(status_code=403, detail="Token 余额不足") - - # 更新 token 使用量 - await update_token_usage(api_key, tokens_required, model_name) - - # 发送任务到Kafka - producer.send(kafka_topic, task_data) - - # 获取更新后的 token 使用情况 - updated_api_key_info = await verify_api_key(api_key) - new_tokens_used = int(updated_api_key_info.get("tokens_used", 0)) - model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0)) - - return { - "message": f"{model_name.upper()}请求已排队等待处理", - "tokens_used": tokens_required, - "total_tokens_used": new_tokens_used, - f"{model_name}_tokens_used": model_tokens_used, - "tokens_remaining": total_tokens - new_tokens_used - } - -class TTSRequest(BaseModel): - text: str - voice: str = Field(..., description="选择的音色") - -class ChatRequest(BaseModel): - session_id: str - query: str - model: str = "qwen2.5:3b" - - -@v1_chat_app.post("/tts") -async def tts_request(request: TTSRequest, api_key_info: dict = Depends(verify_api_key)): - task_id = str(uuid.uuid4()) - text_hash = get_audio_hash(request.text) - - # 验证音色选择 - valid_voices = ["default", "girl", "woman", "man", "leijun", "dufu", "hejiong", "mahuateng", "lidan", "yuhua", "liuzhenyun", "dabing", "luoxiang", "xuzhiyuan"] - if request.voice not in valid_voices: - raise HTTPException(status_code=400, detail="无效的音色选择") - - # 如果声音是 'default',则将其视为 'girl' - voice = 'girl' if request.voice == 'default' else request.voice - - # 使用对应音色的 Redis 客户端 - redis_tts = voice_to_redis[request.voice] - - # 检查是否已存在相同内容的音频文件 - existing_audio_info = redis_tts.get(f"tts:{text_hash}") - if existing_audio_info: - existing_audio_path = json.loads(existing_audio_info)['path'] - if os.path.exists(existing_audio_path): - return { - "message": "TTS请求已完成", - "task_id": task_id, - "status": "completed", - "audio_path": existing_audio_path - } - - # 如果不存在,创建新的任务 - task_data = { - "task_id": task_id, - "text": request.text, - "text_hash": text_hash, - "voice": request.voice, - "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), - } - - # 存储任务信息到Redis - redis_task_client.set(f"task_status:tts:{task_id}", "queued") - - result = await process_request(api_key_info, "tts", 1, task_data, KAFKA_TTS_TOPIC) - result["task_id"] = task_id - return result -@v1_chat_app.post("/asr") -async def asr_request(audio: UploadFile = File(...), api_key_info: dict = Depends(verify_api_key)): - task_id = str(uuid.uuid4()) - - UPLOAD_DIR = "/obscura/task/audio_upload" - os.makedirs(UPLOAD_DIR, exist_ok=True) - file_path = os.path.join(UPLOAD_DIR, f"{task_id}.wav") - - with open(file_path, "wb") as temp_audio: - content = await audio.read() - temp_audio.write(content) - - task_data = { - 'file_path': file_path, - 'task_id': task_id, - 'status': 'queued' - } - - # 存储任务状态,使用一致的键名格式 - redis_task_client.set(f"task_status:asr:{task_id}", "queued") - - result = await process_request(api_key_info, "asr", 1, task_data, KAFKA_ASR_TOPIC) - result["task_id"] = task_id - return result - -@v1_chat_app.post("/chat") -async def chat_request(request: ChatRequest, api_key_info: dict = Depends(verify_api_key)): - task_id = str(uuid.uuid4()) - task_data = { - "task_id": task_id, - "session_id": request.session_id, - "query": request.query, - "model": request.model, - "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), - } - - # 设置任务状态为 "queued" - redis_task_client.set(f"chat:{task_id}:status", "queued") - - result = await process_request(api_key_info, "chat", 1, task_data, KAFKA_CHAT_TOPIC) - result["task_id"] = task_id - return result - - -@v1_chat_app.get("/chat_result/{task_id}") -async def get_chat_result(task_id: str, api_key_info: dict = Depends(verify_api_key)): - # 从Redis任务数据库获取任务状态 - task_status = redis_task_client.get(f"chat:{task_id}:status") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - # 从Redis任务数据库获取聊天结果 - chat_result = redis_task_client.get(f"chat:{task_id}:result") - if chat_result: - result = json.loads(chat_result) - return { - "status": "completed", - "result": result - } - return {"status": status} - - return {"status": "not_found"} - -@v1_chat_app.get("/tts_result/{task_id}") -async def get_tts_result(task_id: str, api_key_info: dict = Depends(verify_api_key)): - task_status = redis_task_client.get(f"task_status:tts:{task_id}") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - task_info = redis_task_client.get(f"task_info:tts:{task_id}") - if task_info: - task_data = json.loads(task_info) - text_hash = task_data['text_hash'] - voice = task_data['voice'] - # 'default' 和 'girl' 都使用 girl 的 Redis - redis_tts = voice_to_redis['girl'] if voice in ['default', 'girl'] else voice_to_redis[voice] - - audio_info = redis_tts.get(f"tts:{text_hash}") - if audio_info: - audio_path = json.loads(audio_info)['path'] - return { - "status": "completed", - "audio_path": audio_path - } - return {"status": status} - - return {"status": "not_found"} - -@v1_chat_app.get("/asr_result/{task_id}") -async def get_asr_result(task_id: str, api_key_info: dict = Depends(verify_api_key)): - # 从Redis任务数据库获取任务状态,使用一致的键名格式 - task_status = redis_task_client.get(f"task_status:asr:{task_id}") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - # 从Redis ASR结果数据库获取转录结果 - transcription = redis_asr_client.get(f"asr:{task_id}") - if transcription: - return { - "status": "completed", - "transcription": transcription.decode('utf-8') - } - return {"status": status} - - return {"status": "not_found"} - -@v1_chat_app.get("/tts_audio/{task_id}") -async def get_tts_audio(task_id: str, api_key_info: dict = Depends(verify_api_key)): - task_status = redis_task_client.get(f"task_status:tts:{task_id}") - if task_status: - status = task_status.decode('utf-8') - if status == "completed": - # 从任务信息中获取使用的音色 - task_info = redis_task_client.get(f"task_info:tts:{task_id}") - if task_info: - task_data = json.loads(task_info) - voice = task_data.get('voice', 'girl') # 默认使用 'girl' - # 'default' 和 'girl' 都使用 girl 的 Redis - redis_tts = voice_to_redis['girl'] if voice in ['default', 'girl'] else voice_to_redis[voice] - - # 从对应音色的 Redis 获取音频文件路径 - audio_info = redis_tts.get(f"tts:{task_data['text_hash']}") - if audio_info: - audio_path = json.loads(audio_info)['path'] - if os.path.exists(audio_path): - file_name = os.path.basename(audio_path) - return FileResponse(audio_path, media_type="audio/wav", filename=file_name) - else: - raise HTTPException(status_code=404, detail="音频文件不存在") - elif status == "queued" or status == "processing": - raise HTTPException(status_code=202, detail="音频文件正在生成中") - else: - raise HTTPException(status_code=500, detail="任务处理失败") - - raise HTTPException(status_code=404, detail="任务不存在") -@v1_chat_app.get("/getvoice") -async def get_available_voices(api_key_info: dict = Depends(verify_api_key)): - valid_voices = ["default", "girl", "woman", "man", "leijun", "dufu", "hejiong", "mahuateng", "lidan", "yuhua", "liuzhenyun", "dabing", "luoxiang", "xuzhiyuan"] - return {"available_voices": valid_voices} - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8008) - - diff --git a/chat_history/qwen_service.py b/chat_history/qwen_service.py deleted file mode 100644 index 5e9ce48..0000000 --- a/chat_history/qwen_service.py +++ /dev/null @@ -1,96 +0,0 @@ -# from flask import Flask, request, jsonify -# import requests -# import json -# from flask_cors import CORS - -# app = Flask(__name__) -# CORS(app) -# DEFAULT_SYSTEM_PROMPT = "你是一个智能助手,请用尽可能简短、简洁、友好的方式回答问题。" - -# @app.route('/chat', methods=['POST']) -# def chat(): -# data = request.json -# query = data.get('query') -# history = data.get('history', []) -# model = data.get('model', 'qwen2') - -# full_prompt = f"{DEFAULT_SYSTEM_PROMPT}\n用户: {query}" - -# data = { -# "model": model, -# "prompt": full_prompt, -# "stream": True, -# "temperature": 0 -# } - -# try: -# response = requests.post("http://127.0.0.1:11434/api/generate", json=data, stream=True) -# response.raise_for_status() - -# text_output = "" -# for line in response.iter_lines(): -# if line: -# json_data = json.loads(line) -# if 'response' in json_data: -# text_output += json_data['response'] - -# final_history = history + [(query, text_output)] - -# return jsonify({"response": text_output, "history": final_history}) - -# except Exception as e: -# return jsonify({"error": str(e)}), 500 - -# if __name__ == '__main__': -# app.run(port=5001) - - -from flask import Flask, request, jsonify -import requests -import json -from flask_cors import CORS - -app = Flask(__name__) -CORS(app) -DEFAULT_SYSTEM_PROMPT = "你是一个智能助手,请用尽可能简短、简洁、友好的方式回答问题。输入的所有内容都来自于语音识别输入,因此可能会出现各种错误,请尽可能猜测用户的意思" - -@app.route('/chat', methods=['POST']) -def chat(): - data = request.json - query = data.get('query') - history = data.get('history', []) - model = data.get('model', 'qwen2') - - # 构建包含历史对话的完整提示 - full_prompt = DEFAULT_SYSTEM_PROMPT + "\n" - for past_query, past_response in history: - full_prompt += f"用户: {past_query}\n助手: {past_response}\n" - full_prompt += f"用户: {query}" - - data = { - "model": model, - "prompt": full_prompt, - "stream": True, - "temperature": 0 - } - - try: - response = requests.post("http://127.0.0.1:11434/api/generate", json=data, stream=True) - response.raise_for_status() - - text_output = "" - for line in response.iter_lines(): - if line: - json_data = json.loads(line) - if 'response' in json_data: - text_output += json_data['response'] - - final_history = history + [(query, text_output)] - - return jsonify({"response": text_output, "history": final_history}) - - except Exception as e: - return jsonify({"error": str(e)}), 500 - -if __name__ == '__main__': - app.run(port=5001) \ No newline at end of file diff --git a/chat_history/sovit.py b/chat_history/sovit.py deleted file mode 100644 index 7e930db..0000000 --- a/chat_history/sovit.py +++ /dev/null @@ -1,49 +0,0 @@ -import argparse -import os -import soundfile as sf - -from tools.i18n.i18n import I18nAuto -from GPT_SoVITS.inference_webui import change_gpt_weights, change_sovits_weights, get_tts_wav - -i18n = I18nAuto() - -def synthesize(GPT_model_path, SoVITS_model_path, ref_audio_path, ref_text_path, ref_language, target_text, target_language, output_path, output_filename): - # Read reference text - with open(ref_text_path, 'r', encoding='utf-8') as file: - ref_text = file.read() - - # Change model weights - change_gpt_weights(gpt_path=GPT_model_path) - change_sovits_weights(sovits_path=SoVITS_model_path) - - # Synthesize audio - synthesis_result = get_tts_wav(ref_wav_path=ref_audio_path, - prompt_text=ref_text, - prompt_language=i18n(ref_language), - text=target_text, - text_language=i18n(target_language), top_p=1, temperature=1) - - result_list = list(synthesis_result) - - if result_list: - last_sampling_rate, last_audio_data = result_list[-1] - output_wav_path = os.path.join(output_path, output_filename) - sf.write(output_wav_path, last_audio_data, last_sampling_rate) - print(f"Audio saved to {output_wav_path}") - -def main(): - GPT_model_path = "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s1bert25hz-5kh-longer-epoch=12-step=369668.ckpt" - SoVITS_model_path = "GPT_SoVITS/pretrained_models/gsv-v2final-pretrained/s2G2333k.pth" - ref_audio_path = "/home/zydi//worker_chat/kafka/sample/woman.wav" - ref_text_path = "/home/zydi//worker_chat/kafka/sample/woman.txt" - ref_language = "中文" - target_text = """我们开发了"病人实时健康监测系统"和"AI辅助诊断系统",这些系统显著提高了医疗诊断的效率和准确性。obscura形成了全面的医疗智能解决方案""" - - target_language = "多语种混合" - output_path = "/home/zydi//worker_chat/kafka" - output_filename = "output.wav" - - synthesize(GPT_model_path, SoVITS_model_path, ref_audio_path, ref_text_path, ref_language, target_text, target_language, output_path, output_filename) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/chat_history/whisper_service.py b/chat_history/whisper_service.py deleted file mode 100644 index 8d43a7a..0000000 --- a/chat_history/whisper_service.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import Flask, request, jsonify -import whisper -import tempfile -from flask_cors import CORS - -app = Flask(__name__) -CORS(app) -print("Loading Whisper model...") -model = whisper.load_model("small") -print("Whisper model loaded.") - -@app.route('/transcribe', methods=['POST']) -def transcribe(): - if 'audio' not in request.files: - return jsonify({"error": "No audio file provided"}), 400 - - audio_file = request.files['audio'] - with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_audio: - audio_file.save(temp_audio.name) - result = model.transcribe(temp_audio.name) - - return jsonify({"transcription": result['text']}) - -if __name__ == '__main__': - app.run(port=5000) \ No newline at end of file diff --git a/producer/__pycache__/config.cpython-311.pyc b/producer/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..724fd1b50add98ab2db50a4594b1e061f9836628 GIT binary patch literal 1721 zcmaJ=OLN**6u!bX#scv&w#mb<_z{y91T={gPur<%utSV30wzhd(I{MjDG#ZVunQ{| zUGyJxlSNnUblM-|%yhwMm6@(I*)Wq$C##+-+37&tUd{c^cfND(x%ZyY{e@v@1S9nH zT`f9|(4WM)8oo>N{x2XuB8)KAL;}Jtfx;7l3r`9YcuJVWZea>f3vTQYrtys6!L%@g zy#nn*GWy1QRZO12zTcUjT?qXK(-{;R%b~B!!G6Jq0|JACf**&301gX5JS&87L z73MHiLb13-4tv}Sr_<>~>e0hQI+=L5z4JKvIBibVG^L~Vn2}KZT@<07kAJ0CI-2xV zI+?z2EkOxunZOD_FTyHLlr<#1!FJT9BH?jWll4y1IEhiFNB*v^8=}(a-TJt&rC_0kJ8yaZAz~vX&qX=uhN|zdY_(d`ypa&znM zC9TNT7NIjO*a!}#%2tf{({M;t(q!u%crjY@fYHcn()<+%58l*`hh;cn3wkTo7{^u~5wJ=U$q0F`LPi#3y{| zJE^!B0qS56-p64=RCRZed zOXd8D%{?ut0r%H*Gw?N^=fxvFlg*0-?&OIXDDb81h(Lb05n-Rp=f{M-ktLBMxDOnw zFrw0YKF=L-V!4<%{cs;>h`7fe@8=FmCbL)0aAGF=JO`DMqi*U~^d1_FKBU<5^K&va zMKM?`pQRF)Z!&SNXbo1?6x^xE8e37?)p{+Vc0YJNhey|G%3o^RP=SJ}tAN7^ilT;Y zMA7{j@&OI5_kDxt@&%d$$G_O82TTYcwA}X(g5f?h2!tSEZs7uj-!f|?3_wcw`mXQ2 zKRo>@a)CDApv^zfCXfO0UMEBTY_cC1gzppJ2z++sE_`u;B5#=-RHadLX%LwoPE9cG zAtG?Cvf|_fNDctxY&psTQ2;<$bQB*^06^Jrln7A(uC4s<-PfE|VX_JUR;@ZpkSG9$ cR4nWoB7nYs_>w}Q8_=_0w9k-FdjOsP8|A+9L;wH) literal 0 HcmV?d00001 diff --git a/api/producer/config.py b/producer/config.py old mode 100644 new mode 100755 similarity index 65% rename from api/producer/config.py rename to producer/config.py index 049a8c2..5a6714f --- a/api/producer/config.py +++ b/producer/config.py @@ -3,16 +3,16 @@ import os # Kafka配置 -KAFKA_BROKER = "222.186.10.253:9092" +KAFKA_BROKER = "222.186.20.67:9092" KAFKA_GROUP_ID_PREFIX = "group" # Redis配置 -REDIS_HOST = "150.158.144.159" -REDIS_PORT = 13003 +REDIS_HOST = "222.186.20.67" +REDIS_PORT = 6379 REDIS_PASSWORD = "Obscura@2024" -MAIN_REDIS_DB = 0 -REDIS_API_DB = 2 -REDIS_API_USAGE_DB = 3 +MAIN_REDIS_DB = 30 +REDIS_API_DB = 31 +REDIS_API_USAGE_DB = 32 # 目录配置 UPLOAD_DIR = "/obscura/task/upload" RESULT_DIR = "/obscura/task/result" @@ -22,11 +22,11 @@ os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(RESULT_DIR, exist_ok=True) # 模型配置 -YOLO_MODEL_PATH = "/obscura/models/yolov8x.pt" -POSE_MODEL_PATH = "/obscura/models/yolov8x-pose.pt" -QWEN_MODEL_PATH = "/obscura/models/qwen/Qwen2-VL-2B-Instruct" +YOLO_MODEL_PATH = "/obscura/models/yolov11n.pt" +POSE_MODEL_PATH = "/obscura/models/yolov11n-pose.pt" +QWEN_MODEL_PATH = "/obscura/models/QWEN/Qwen2___5-VL-7B-Instruct" FALL_MODEL_PATH = "/obscura/models/yolov8n-fall.pt" -FACE_MODEL_PATH = "/obscura/models/yolov8n-face.pt" +FACE_MODEL_PATH = "/obscura/models/yolov11n-face.pt" MEDIAPIPE_MODEL_PATH = "/obscura/models/face_landmarker.task" # Ollama配置 OLLAMA_URL = "https://ffgregevrdcfyhtnhyudvr.myfastools.com/api/generate" @@ -35,46 +35,46 @@ OLLAMA_URL = "https://ffgregevrdcfyhtnhyudvr.myfastools.com/api/generate" WORKER_CONFIGS = { "yolo": { "kafka_topic": "yolo", - "redis_db": 4, + "redis_db": 33, }, "pose": { "kafka_topic": "pose", - "redis_db": 5, + "redis_db": 34, }, "qwenvl": { "kafka_topic": "qwenvl", - "redis_db": 9, + "redis_db": 35, }, "qwenvl_analyze": { "kafka_topic": "qwenvl_analyze", - "redis_db": 32, + "redis_db": 36, }, "cpm": { "kafka_topic": "cpm", - "redis_db": 8, + "redis_db": 37, }, "cpm_analyze": { "kafka_topic": "cpm_analyze", - "redis_db": 31, + "redis_db": 38, }, "fall": { "kafka_topic": "fall", - "redis_db": 6, + "redis_db": 39, }, "face": { "kafka_topic": "face", - "redis_db": 7, + "redis_db": 40, }, "mediapipe": { "kafka_topic": "mediapipe", - "redis_db": 10, + "redis_db": 41, }, "compare": { "kafka_topic": "compare", - "redis_db": 30, + "redis_db": 42, } } # GPU设置 CUDA_DEVICE_0 = "cuda:0" -CUDA_DEVICE_1 = "cuda:1" +CUDA_DEVICE_1 = "cuda:1" \ No newline at end of file diff --git a/api/producer/producer.py b/producer/producer.py old mode 100644 new mode 100755 similarity index 100% rename from api/producer/producer.py rename to producer/producer.py diff --git a/api/producer/requirements.txt b/producer/requirements.txt old mode 100644 new mode 100755 similarity index 100% rename from api/producer/requirements.txt rename to producer/requirements.txt diff --git a/api_chat/producer_chat/.env b/producer_chat/.env old mode 100644 new mode 100755 similarity index 67% rename from api_chat/producer_chat/.env rename to producer_chat/.env index b6516d9..dde182b --- a/api_chat/producer_chat/.env +++ b/producer_chat/.env @@ -1,28 +1,27 @@ # Kafka 配置 -KAFKA_BROKER=222.186.10.253:9092 +KAFKA_BROKER=222.186.20.67:9092 KAFKA_ASR_TOPIC=asr KAFKA_CHAT_TOPIC=chat KAFKA_TTS_TOPIC=tts # Redis 配置 -REDIS_HOST=150.158.144.159 -REDIS_PORT=13003 -REDIS_ASR_DB=12 -REDIS_CHAT_DB=13 -REDIS_TTS_DB=14 +REDIS_HOST=222.186.20.67 +REDIS_PORT=6379 +REDIS_ASR_DB=43 +REDIS_CHAT_DB=44 +REDIS_TTS_DB=45 REDIS_PASSWORD=Obscura@2024 -REDIS_API_DB=2 -REDIS_API_USAGE_DB=3 -REDIS_TASK_DB=11 -REDIS_SESSION_DB=63 +REDIS_API_DB=31 +REDIS_API_USAGE_DB=32 +REDIS_TASK_DB=46 +REDIS_SESSION_DB=47 + +REDIS_SESSION_DB_ZH=48 +REDIS_SESSION_DB_EN=49 +REDIS_SESSION_DB_KO=50 -REDIS_SESSION_DB_ZH=63 -REDIS_SESSION_DB_EN=62 -REDIS_SESSION_DB_KO=61 -# CORS 配置 -# ALLOWED_ORIGINS=https://beta.obscura.work # GPT-SoVITS 配置 @@ -76,19 +75,18 @@ XUZHIYUAN_REF_AUDIO=sample/xuzhiyuan.wav XUZHIYUAN_REF_TEXT=sample/xuzhiyuan.txt -REDIS_GIRL_DB = 15 -REDIS_WOMAN_DB = 16 -REDIS_MAN_DB = 17 -REDIS_LEIJUN_DB = 18 -REDIS_DUFU_DB = 19 -REDIS_HEJIONG_DB = 20 -REDIS_MAHUATENG_DB = 21 -REDIS_LIDAN_DB = 22 -REDIS_DABING_DB = 23 -REDIS_LUOXIANG_DB = 24 -REDIS_XUZHIYUAN_DB = 25 -REDIS_YUHUA_DB = 26 -REDIS_LIUZHENYUN_DB = 27 - +REDIS_GIRL_DB = 51 +REDIS_WOMAN_DB = 52 +REDIS_MAN_DB = 53 +REDIS_LEIJUN_DB = 54 +REDIS_DUFU_DB = 55 +REDIS_HEJIONG_DB = 56 +REDIS_MAHUATENG_DB = 57 +REDIS_LIDAN_DB = 58 +REDIS_DABING_DB = 59 +REDIS_LUOXIANG_DB = 60 +REDIS_XUZHIYUAN_DB = 61 +REDIS_YUHUA_DB = 62 +REDIS_LIUZHENYUN_DB = 63 diff --git a/api_chat/producer_chat/producer_chat.py b/producer_chat/producer_chat.py old mode 100644 new mode 100755 similarity index 100% rename from api_chat/producer_chat/producer_chat.py rename to producer_chat/producer_chat.py diff --git a/api_chat/producer_chat/requirements.txt b/producer_chat/requirements.txt old mode 100644 new mode 100755 similarity index 100% rename from api_chat/producer_chat/requirements.txt rename to producer_chat/requirements.txt