H022329 commited on
Commit
9cf08e9
·
verified ·
1 Parent(s): 618ea76

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +47 -0
  2. .gitattributes +7 -0
  3. .gitignore +2 -0
  4. .vscode/settings.json +4 -0
  5. README.md +160 -0
  6. VideoAgent/__init__.py +1 -0
  7. VideoAgent/__pycache__/__init__.cpython-310.pyc +0 -0
  8. VideoAgent/__pycache__/_utils.cpython-310.pyc +0 -0
  9. VideoAgent/__pycache__/base.cpython-310.pyc +0 -0
  10. VideoAgent/__pycache__/chunk.cpython-310.pyc +0 -0
  11. VideoAgent/__pycache__/prompt.cpython-310.pyc +0 -0
  12. VideoAgent/__pycache__/query.cpython-310.pyc +0 -0
  13. VideoAgent/__pycache__/vidrag_pipeline.cpython-310.pyc +0 -0
  14. VideoAgent/_llm/__init__.py +6 -0
  15. VideoAgent/_llm/__pycache__/__init__.cpython-310.pyc +0 -0
  16. VideoAgent/_llm/__pycache__/asr_model.cpython-310.pyc +0 -0
  17. VideoAgent/_llm/__pycache__/embedding_client.cpython-310.pyc +0 -0
  18. VideoAgent/_llm/__pycache__/embedding_mode_openai.cpython-310.pyc +0 -0
  19. VideoAgent/_llm/__pycache__/embedding_model.cpython-310.pyc +0 -0
  20. VideoAgent/_llm/__pycache__/llm_model.cpython-310.pyc +0 -0
  21. VideoAgent/_llm/__pycache__/qwen3vl_embedding_client.cpython-310.pyc +0 -0
  22. VideoAgent/_llm/__pycache__/tokenizer_model.cpython-310.pyc +0 -0
  23. VideoAgent/_llm/__pycache__/vlm_model.cpython-310.pyc +0 -0
  24. VideoAgent/_llm/asr_model.py +177 -0
  25. VideoAgent/_llm/embedding_model.py +60 -0
  26. VideoAgent/_llm/llm_model.py +80 -0
  27. VideoAgent/_llm/tokenizer_model.py +115 -0
  28. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/tokenizer.json +3 -0
  29. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/tokenizer_config.json +239 -0
  30. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/vocab.json +0 -0
  31. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/.mdl +0 -0
  32. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/.msc +0 -0
  33. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/tokenizer.json +3 -0
  34. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/tokenizer_config.json +239 -0
  35. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/vocab.json +0 -0
  36. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/.mdl +0 -0
  37. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/.msc +0 -0
  38. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/tokenizer.json +3 -0
  39. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/tokenizer_config.json +239 -0
  40. VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/vocab.json +0 -0
  41. VideoAgent/_llm/vlm_model.py +54 -0
  42. VideoAgent/_server/embedding_server.py +347 -0
  43. VideoAgent/_server/llm_server.py +45 -0
  44. VideoAgent/_server/qwen3_vl_embedding.py +407 -0
  45. VideoAgent/_server/sherpa_asr_server.py +180 -0
  46. VideoAgent/_server/tokenizer_server.py +121 -0
  47. VideoAgent/_server/vlm_server.py +45 -0
  48. VideoAgent/_storage/__init__.py +2 -0
  49. VideoAgent/_storage/__pycache__/__init__.cpython-310.pyc +0 -0
  50. VideoAgent/_storage/__pycache__/kv_json.cpython-310.pyc +0 -0
.env.example ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # # LLM API(OpenAI API格式)
3
+ # EMBEDDING_MODEL_PATH = "/data/huangjie/.cache/modelscope/hub/models/Qwen/Qwen3-VL-Embedding-2B"
4
+ EMBEDDING_API_BASE_URL = "http://10.126.102.211:8010/v1/"
5
+ # EMBEDDING_API_KEY = "xxx"
6
+ EMBEDDING_API_PORT = 8010
7
+ EMBEDDING_MODEL_NAME = "AXERA-TECH/Qwen3-VL-Embedding-2B"
8
+
9
+
10
+ # # VLM API(OpenAI API格式)
11
+ # VLM_MODEL_PATH = "/data/huangjie/.cache/modelscope/hub/models/Qwen/Qwen3-VL-2B-Instruct"
12
+ VLM_API_BASE_URL = "http://10.126.102.211:8011/v1/"
13
+ # VLM_API_KEY = "xxxx"
14
+ VLM_API_PORT = 8011
15
+ VLM_MODEL_NAME = "AXERA-TECH/Qwen3-VL-2B-Instruct"
16
+
17
+
18
+ # # LLM API(OpenAI API格式)
19
+ # LLM_MODEL_PATH = "/data/huangjie/.cache/modelscope/hub/models/Qwen/Qwen3-VL-Embedding-2B"
20
+ LLM_API_BASE_URL = "http://10.126.102.211:8012/v1/"
21
+ # LLM_API_KEY = "xxx"
22
+ LLM_API_PORT = 8012
23
+ LLM_MODEL_NAME = "AXERA-TECH/Qwen3-1.7B"
24
+
25
+
26
+ # # ASR API
27
+ SHERPA_MODEL_DIR = "/root/huangjie/AXERA-TECH/SenseVoice"
28
+ SHERPA_ASR_URL = "http://10.126.102.211:8013"
29
+ # ASR_API_KEY = "xxx"
30
+ SHERPA_ASR_API_PORT = 8013
31
+ SHERPA_MODEL_FILE = "/root/huangjie/AXERA-TECH/SenseVoice/ax650/model-10-seconds.axmodel"
32
+
33
+
34
+ # # Tokenizer API
35
+ Tokenizer_MODEL_PATH = "./VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B" # Tokenizer 文件夹(含tokenizer_config.json, tokenizer.json, vocab.json文件)
36
+ Tokenizer_API_BASE_URL = "http://0.0.0.0:8014/"
37
+ Tokenizer_API_KEY = "xxxx"
38
+ Tokenizer_API_PORT = 8014
39
+
40
+
41
+
42
+ VIDEORAG_VIDEO_SEGMENT_LENGTH = "10"
43
+ VIDEORAG_ROUGH_NUM_FRAMES_PER_SEGMENT = "5"
44
+ VIDEORAG_RETRIEVAL_TOPK_CHUNKS = "2"
45
+ VIDEORAG_QUERY_BETTER_THAN_THRESHOLD = "0.2"
46
+ VIDEORAG_CHUNK_TOKEN_SIZE = "800"
47
+ VIDEORAG_SEGMENT_RETRIEVAL_TOP_K = "2"
.gitattributes CHANGED
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ image-4.png filter=lfs diff=lfs merge=lfs -text
37
+ image-5.png filter=lfs diff=lfs merge=lfs -text
38
+ VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/tokenizer.json filter=lfs diff=lfs merge=lfs -text
39
+ VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/tokenizer.json filter=lfs diff=lfs merge=lfs -text
40
+ VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/tokenizer.json filter=lfs diff=lfs merge=lfs -text
41
+ videos/origin/sanguo.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ videos/processed/sanguo.mp4 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .env
2
+ uphg.py
.vscode/settings.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "python-envs.defaultEnvManager": "ms-python.python:conda",
3
+ "python-envs.defaultPackageManager": "ms-python.python:conda"
4
+ }
README.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VideoAgent — 视频理解分析(基于 AX650N)
2
+
3
+ 基于 AX650N 芯片平台,构建多模态 VideoAgent,面向视频理解与检索,支持长视频智能分析与自然语言问答。
4
+
5
+ <p align="center">
6
+ <img src="https://img.shields.io/badge/platform-AX650N-blue" alt="Platform">
7
+ <img src="https://img.shields.io/badge/python-3.10+-green" alt="Python">
8
+
9
+ </p>
10
+
11
+ ---
12
+
13
+ ## 核心功能
14
+
15
+ - **芯片平台部署** — 基于 AX650N 芯片部署全部模型,端到端运行完整流程
16
+ - **视频智能索引** — 自动分段、特征提取、多模态信息融合(ASR + VLM)
17
+ - **向量检索** — 高效相似度检索与结果融合,支持跨模态查询
18
+ - **自然语言问答** — 用自然语言提问,基于视频内容生成回答
19
+
20
+ ---
21
+
22
+ ## 模型配置
23
+
24
+ 基于 AX650N 芯片平台运行前,请下载以下模型并参照相关文档完成部署:
25
+
26
+ | 模型类型 | 模型名称 | 说明 |
27
+ |---------|---------|------|
28
+ | **ASR** | [SenseVoiceSmall-axmodel](https://huggingface.co/M5Stack/SenseVoiceSmall-axmodel) | 多语言语音理解模型 |
29
+ | **VLM** | [Qwen3-VL-2B-Instruct-GPTQ-Int4](https://huggingface.co/AXERA-TECH/Qwen3-VL-2B-Instruct-GPTQ-Int4) | 多模态视觉语言模型 |
30
+ | **LLM** | [Qwen3-1.7B](https://huggingface.co/AXERA-TECH/Qwen3-1.7B) | 大语言模型 |
31
+ | **Embedding** | [Qwen3-VL-Embedding-2B-AX650](https://huggingface.co/AXERA-TECH/Qwen3-VL-Embedding-2B-AX650-C128_P1280_CTX1407) | 多模态嵌入模型 |
32
+
33
+ ---
34
+
35
+ ## 快速开始
36
+
37
+ ### 1. 安装依赖
38
+
39
+ ```bash
40
+ pip install -r requirements.txt
41
+ ```
42
+
43
+ ### 2. 配置环境变量
44
+
45
+ Embedding、VLM、LLM、ASR、Tokenizer 均通过环境变量配置。其中 Embedding、VLM、LLM 兼容 OpenAI API 格式。
46
+
47
+ ```bash
48
+ cp .env.example .env
49
+ # 编辑 .env 文件,填入实际模型路径和 API 地址
50
+ ```
51
+
52
+ `.env` 配置示例:
53
+
54
+ ```ini
55
+ # LLM API(OpenAI API 格式)
56
+ LLM_MODEL_PATH = "/data/huangjie/.cache/modelscope/hub/models/Qwen/Qwen3-VL-Embedding-2B"
57
+ LLM_API_BASE_URL = "http://0.0.0.0:8012/v1/"
58
+ LLM_API_KEY = "xxx"
59
+ LLM_MODEL_NAME = "AXERA-TECH/Qwen3-1.7B"
60
+ LLM_API_PORT = 8012
61
+
62
+ # ASR API
63
+ SHERPA_MODEL_DIR = "/root/huangjie/AXERA-TECH/SenseVoice"
64
+ SHERPA_ASR_URL = "http://0.0.0.0:8013"
65
+ SHERPA_ASR_API_PORT = 8013
66
+ SHERPA_MODEL_FILE = "/root/huangjie/AXERA-TECH/SenseVoice/ax650/model-10-seconds.axmodel"
67
+
68
+ # Tokenizer API
69
+ Tokenizer_MODEL_PATH = "/root/huangjie/project/VideoAgent_api507/VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B"
70
+ Tokenizer_API_BASE_URL = "http://0.0.0.0:8014"
71
+ Tokenizer_API_PORT = 8014
72
+ ```
73
+
74
+ ### 3. 启动模型服务
75
+
76
+ 基于 AX650N 芯片启动各模型服务:
77
+
78
+ ```bash
79
+ # Embedding 服务 — 端口 8010
80
+ axllm serve /root/huangjie/AXERA-TECH/models--AXERA-TECH--Qwen3-VL-Embedding-2B-AX650-C128_P1280_CTX1407 --port 8010
81
+
82
+ # VLM 服务 — 端口 8011
83
+ axllm serve /root/huangjie/AXERA-TECH/Qwen3-VL-2B-Instruct-GPTQ-Int4 --port 8011
84
+
85
+ # LLM 服务 — 端口 8012
86
+ axllm serve /root/huangjie/AXERA-TECH/models--AXERA-TECH--Qwen3-1.7B --port 8012
87
+
88
+ # ASR 服务 — 端口 8013
89
+ python VideoAgent/_server/sherpa_asr_server.py
90
+
91
+ # Tokenizer 服务 — 端口 8014
92
+ python VideoAgent/_server/tokenizer_server.py
93
+ ```
94
+
95
+ ### 4. 使用方式
96
+
97
+ #### Web UI(推荐)
98
+
99
+ ```bash
100
+ python webui.py
101
+ ```
102
+
103
+ 浏览器访问 **http://localhost:7869**
104
+
105
+ | 索引界面 | 检索界面 |
106
+ |---------|---------|
107
+ | ![索引界面](image-5.png) | ![检索界面](image-4.png) |
108
+
109
+ #### Python SDK
110
+
111
+ ```python
112
+ from VideoAgent import VideoRAG, QueryParam
113
+
114
+ # 初始化 RAG 系统
115
+ rag = VideoRAG(working_dir="./working_dir")
116
+
117
+ # 索引视频文件
118
+ rag.insert_video(video_path_list=["video1.mp4", "video2.mp4"])
119
+
120
+ # 查询视频内容
121
+ result = rag.query(query="视频中什么时候出现张飞?", param=QueryParam())
122
+ print(result)
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 工作流程
128
+
129
+ ### 视频索引流程
130
+
131
+ ![索引流程](mermaid-1778811634860.png)
132
+ ### 查询流程
133
+
134
+
135
+ ![查询流程](image-3.png)
136
+
137
+
138
+ ---
139
+
140
+ ## 项目结构
141
+
142
+ ```
143
+ VideoAgent-AX650N/
144
+ ├── VideoAgent/ # 核心包
145
+ │ ├── _llm/ # 模型定义层
146
+ │ ├── _server/ # 服务层(FastAPI)
147
+ │ ├── _storage/ # 存储层
148
+ │ ├── _videoutil/ # 视频处理工具
149
+ │ └── vidrag_pipeline.py # 核心管道
150
+ ├── working_dir/ # 运行时数据目录
151
+ ├── webui.py # Gradio Web 入口
152
+ ├── videorag_longervideos.py # 测试脚本
153
+ └── README.md # 项目文档
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 参考项目
159
+
160
+ - 香港大学数据科学实验室(HKUDS)— [VideoRAG](https://github.com/HKUDS/VideoRAG):超长视频跨模态检索增强生成框架
VideoAgent/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .vidrag_pipeline import VideoRAG, QueryParam
VideoAgent/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (225 Bytes). View file
 
VideoAgent/__pycache__/_utils.cpython-310.pyc ADDED
Binary file (3.18 kB). View file
 
VideoAgent/__pycache__/base.cpython-310.pyc ADDED
Binary file (3.84 kB). View file
 
VideoAgent/__pycache__/chunk.cpython-310.pyc ADDED
Binary file (2.22 kB). View file
 
VideoAgent/__pycache__/prompt.cpython-310.pyc ADDED
Binary file (2.99 kB). View file
 
VideoAgent/__pycache__/query.cpython-310.pyc ADDED
Binary file (5.23 kB). View file
 
VideoAgent/__pycache__/vidrag_pipeline.cpython-310.pyc ADDED
Binary file (9.87 kB). View file
 
VideoAgent/_llm/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .vlm_model import Qwen3VLModel
2
+ from .embedding_model import Qwen3VLEmbedderC
3
+
4
+ from .llm_model import Qwen3
5
+ from .asr_model import ASRModel, SherpaOnnxASRClient
6
+ from .tokenizer_model import Qwen3TokenizerClient
VideoAgent/_llm/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (434 Bytes). View file
 
VideoAgent/_llm/__pycache__/asr_model.cpython-310.pyc ADDED
Binary file (5.56 kB). View file
 
VideoAgent/_llm/__pycache__/embedding_client.cpython-310.pyc ADDED
Binary file (6.97 kB). View file
 
VideoAgent/_llm/__pycache__/embedding_mode_openai.cpython-310.pyc ADDED
Binary file (2.23 kB). View file
 
VideoAgent/_llm/__pycache__/embedding_model.cpython-310.pyc ADDED
Binary file (2.22 kB). View file
 
VideoAgent/_llm/__pycache__/llm_model.cpython-310.pyc ADDED
Binary file (2.52 kB). View file
 
VideoAgent/_llm/__pycache__/qwen3vl_embedding_client.cpython-310.pyc ADDED
Binary file (5.71 kB). View file
 
VideoAgent/_llm/__pycache__/tokenizer_model.cpython-310.pyc ADDED
Binary file (3.02 kB). View file
 
VideoAgent/_llm/__pycache__/vlm_model.cpython-310.pyc ADDED
Binary file (1.77 kB). View file
 
VideoAgent/_llm/asr_model.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import os
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ import tempfile
7
+ import librosa
8
+ import numpy as np
9
+ import requests
10
+ from typing import Optional
11
+ from dataclasses import dataclass
12
+ from dotenv import load_dotenv
13
+
14
+ load_dotenv()
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class ASRResult:
20
+ """ASR 识别结果"""
21
+ text: str
22
+ lang: str = ""
23
+ emotion: str = ""
24
+ event: str = ""
25
+ timestamps: list = None
26
+
27
+
28
+ class ASRModel:
29
+ """语音识别模型封装,调用 asr_server 的 /asr 接口"""
30
+
31
+ def __init__(self, base_url: str = None, language: str = "auto", sample_rate: int = 16000):
32
+ """
33
+ 初始化 ASR 模型
34
+
35
+ Args:
36
+ base_url: ASR 服务地址,如 "http://localhost:8000"
37
+ language: 默认语言代码 (auto, zh, en, ja, ko, yue)
38
+ sample_rate: 音频采样率,默认 16000 Hz
39
+ """
40
+ self.base_url = (base_url or os.getenv("ASR_API_BASE_URL", "http://localhost:8000")).rstrip('/')
41
+ self.language = language or os.getenv("ASR_LANGUAGE", "auto")
42
+ self.sample_rate = sample_rate
43
+ self.asr_endpoint = f"{self.base_url}/asr"
44
+
45
+ def _load_audio(self, audio_path: str) -> np.ndarray:
46
+ """
47
+ 加载音频文件并重采样到目标采样率
48
+
49
+ Args:
50
+ audio_path: 音频文件路径
51
+
52
+ Returns:
53
+ 音频数组
54
+ """
55
+ try:
56
+ waveform, sr = librosa.load(audio_path, sr=None, mono=True)
57
+ if sr != self.sample_rate:
58
+ waveform = librosa.resample(waveform, orig_sr=sr, target_sr=self.sample_rate)
59
+ return waveform
60
+ except Exception as e:
61
+ logger.error(f"Failed to load audio file {audio_path}: {e}")
62
+ raise
63
+
64
+ def transcribe(self, audio_file_path: str, language: Optional[str] = None) -> ASRResult:
65
+ """
66
+ 转录音频文件
67
+
68
+ Args:
69
+ audio_file_path: 音频文件路径
70
+ language: 语言代码,不提供则使用默认值
71
+
72
+ Returns:
73
+ ASRResult 对象,包含 text 字段
74
+ """
75
+ lang = language or self.language
76
+
77
+ try:
78
+ # 验证文件存在
79
+ if not os.path.exists(audio_file_path):
80
+ raise FileNotFoundError(f"Audio file not found: {audio_file_path}")
81
+
82
+ # logger.info(f"Transcribing audio file: {audio_file_path}")
83
+
84
+ # 加载音频文件
85
+ waveform = self._load_audio(audio_file_path)
86
+
87
+ # 转换为列表格式用于 API 调用
88
+ audio_data = waveform.tolist()
89
+
90
+ # logger.info(f"Calling ASR API: {self.asr_endpoint}")
91
+ # logger.info(f"Audio length: {len(audio_data)}, Sample rate: {self.sample_rate}, Language: {lang}")
92
+
93
+ # 调用 asr_server 的 /asr 接口
94
+ response = requests.post(
95
+ self.asr_endpoint,
96
+ json={
97
+ "audio_data": audio_data,
98
+ "sample_rate": self.sample_rate,
99
+ "language": lang
100
+ },
101
+ timeout=300
102
+ )
103
+
104
+ if response.status_code != 200:
105
+ error_detail = response.json().get("detail", response.text) if response.headers.get("content-type") == "application/json" else response.text
106
+ logger.error(f"ASR API error: {response.status_code} - {error_detail}")
107
+ raise RuntimeError(f"ASR API error: {response.status_code} - {error_detail}")
108
+
109
+ result_data = response.json()
110
+ text = result_data.get("text", "")
111
+
112
+ # 清理文本中的特殊标记
113
+ text = self._clean_text(text)
114
+
115
+ return ASRResult(text=text)
116
+
117
+ except FileNotFoundError as e:
118
+ logger.error(f"File error: {e}")
119
+ raise
120
+ except requests.RequestException as e:
121
+ logger.error(f"Failed to connect to ASR API: {e}")
122
+ raise RuntimeError(f"Failed to connect to ASR API at {self.asr_endpoint}: {e}")
123
+ except Exception as e:
124
+ logger.error(f"ASR transcription error: {e}")
125
+ raise
126
+
127
+ @staticmethod
128
+ def _clean_text(text: str) -> str:
129
+ """清理 SenseVoice 返回的文本中的特殊标记"""
130
+ # 移除所有 <|....|> 格式的标记
131
+ text = re.sub(r'<\|[^|]*\|>', '', text)
132
+ # 清理多余空格
133
+ text = re.sub(r'\s+', ' ', text).strip()
134
+ return text
135
+
136
+
137
+
138
+ class SherpaOnnxASRClient:
139
+ """调用 Sherpa-ONNX FastAPI 服务的客户端"""
140
+
141
+ def __init__(self, base_url: str = None):
142
+ self.base_url = (base_url or os.getenv("SHERPA_ASR_URL", "http://10.126.102.211:8016")).rstrip("/")
143
+
144
+ def _parse_response(self, response) -> ASRResult:
145
+ if response.status_code != 200:
146
+ raise RuntimeError(f"Sherpa ASR error: {response.status_code} - {response.text}")
147
+ data = response.json()
148
+ # print("lang: ", data.get("lang", ""))
149
+ return ASRResult(
150
+ text=data.get("text", ""),
151
+ lang=data.get("lang", ""),
152
+ emotion=data.get("emotion", ""),
153
+ event=data.get("event", ""),
154
+ timestamps=data.get("timestamps", []),
155
+ )
156
+
157
+ def transcribe(self, audio_path: str) -> ASRResult:
158
+ """通过 /asr/file 接口上传音频文件"""
159
+ if not os.path.exists(audio_path):
160
+ raise FileNotFoundError(f"Audio file not found: {audio_path}")
161
+
162
+ with open(audio_path, "rb") as f:
163
+ response = requests.post(
164
+ f"{self.base_url}/asr/file",
165
+ files={"file": f},
166
+ timeout=120,
167
+ )
168
+ return self._parse_response(response)
169
+
170
+ def transcribe_audio_data(self, audio_data: list, sample_rate: int = 16000) -> ASRResult:
171
+ """通过 /asr 接口发送音频数组数据"""
172
+ response = requests.post(
173
+ f"{self.base_url}/asr",
174
+ json={"audio_data": audio_data, "sample_rate": sample_rate},
175
+ timeout=120,
176
+ )
177
+ return self._parse_response(response)
VideoAgent/_llm/embedding_model.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from modelscope.hub.file_download import model_file_download
4
+ from openai._types import NOT_GIVEN, NotGiven
5
+ from openai.types.chat import ChatCompletionMessageParam
6
+ from openai.types.create_embedding_response import CreateEmbeddingResponse
7
+
8
+ from typing import Literal
9
+
10
+
11
+ class Qwen3VLEmbedderC:
12
+ def __init__(self, model_name: str = None, base_url: str = None, api_key: str = None):
13
+ self.model_name = model_name or os.getenv("EMBEDDING_MODEL_NAME", "Qwen3-4B-Instruct-2507")
14
+ self.base_url = base_url or os.getenv("EMBEDDING_API_BASE_URL", "http://localhost:8000/v1")
15
+ self.api_key = api_key or os.getenv("EMBEDDING_API_KEY", "EMPTY")
16
+ self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
17
+
18
+ def create_chat_embeddings(
19
+ self,
20
+ messages: list[ChatCompletionMessageParam],
21
+ model: str,
22
+ encoding_format: Literal["base64", "float"] | NotGiven = NOT_GIVEN,
23
+ continue_final_message: bool = False,
24
+ add_special_tokens: bool = False,
25
+ ) -> CreateEmbeddingResponse:
26
+ """
27
+ Convenience function for accessing vLLM's Chat Embeddings API,
28
+ which is an extension of OpenAI's existing Embeddings API.
29
+ """
30
+ return self.client.post(
31
+ "/embeddings",
32
+ cast_to=CreateEmbeddingResponse,
33
+ body={
34
+ "messages": messages,
35
+ "model": model,
36
+ "encoding_format": encoding_format,
37
+ "continue_final_message": continue_final_message,
38
+ "add_special_tokens": add_special_tokens,
39
+ },
40
+ )
41
+
42
+ def embedding_gen(self, message_input):
43
+ """
44
+ Convenience function for accessing vLLM's Text Embeddings API,
45
+ which is an extension of OpenAI's existing Embeddings API.
46
+ """
47
+ response = self.create_chat_embeddings(
48
+ messages = message_input,
49
+ model=self.model_name,
50
+ encoding_format="float",
51
+ continue_final_message=False,
52
+ add_special_tokens=True,
53
+ )
54
+
55
+ return response.data[0].embedding
56
+
57
+
58
+
59
+
60
+
VideoAgent/_llm/llm_model.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from modelscope.hub.file_download import model_file_download
4
+ from .._utils import logger
5
+
6
+ class Qwen3:
7
+ def __init__(self, model_name: str = None, base_url: str = None, api_key: str = None):
8
+ self.model_name = model_name or os.getenv("LLM_MODEL_NAME", "Qwen3-4B-Instruct-2507")
9
+ self.base_url = base_url or os.getenv("LLM_API_BASE_URL", "http://localhost:8000/v1")
10
+ self.api_key = api_key or os.getenv("LLM_API_KEY", "EMPTY")
11
+ self.client = OpenAI(base_url=self.base_url, api_key=self.api_key, timeout=250000.0)
12
+
13
+
14
+ def generate_result(self, messages, max_new_tokens: int = 16384) -> str:
15
+ response = self.client.chat.completions.create(
16
+ model=self.model_name,
17
+ messages=messages,
18
+ max_tokens=max_new_tokens,
19
+ extra_body={"enable_thinking": False},
20
+ )
21
+ return response.choices[0].message.content
22
+
23
+ def generate_result_stream(self, messages, max_new_tokens: int = 32768):
24
+ response = self.client.chat.completions.create(
25
+ model=self.model_name,
26
+ messages=messages,
27
+ max_tokens=max_new_tokens,
28
+ stream=True,
29
+ extra_body={"enable_thinking": False},
30
+ )
31
+ for chunk in response:
32
+ delta = chunk.choices[0].delta
33
+ # 遍历 delta 所有属性,收集所有非空字符串字段
34
+ text = None
35
+
36
+ for field in ("content", "reasoning_content", "reasoning", "thinking", "text"):
37
+ val = getattr(delta, field, None)
38
+
39
+ if isinstance(val, str) and val:
40
+ text = val
41
+ break
42
+ # 兜底:如果以上都没有,遍历所有属性找字符串
43
+ if text is None:
44
+ for attr in dir(delta):
45
+ if attr.startswith("_"):
46
+ continue
47
+ try:
48
+ val = getattr(delta, attr)
49
+ if isinstance(val, str) and val:
50
+ text = val
51
+ break
52
+ except Exception:
53
+ pass
54
+ if text:
55
+ yield text
56
+ if chunk.choices[0].finish_reason:
57
+ finish = chunk.choices[0].finish_reason
58
+ if finish == "length":
59
+ yield "\n\n[⚠️ 输出因达到长度限制({} tokens)被截断]".format(max_new_tokens)
60
+ break
61
+
62
+ def generate_result_stream2(self, messages, max_new_tokens: int = 16384):
63
+ response = self.client.chat.completions.create(
64
+ model=self.model_name,
65
+ messages=messages,
66
+
67
+ stream=True,
68
+
69
+ )
70
+ for ev in response:
71
+ delta = getattr(ev.choices[0], "delta", None)
72
+ # print(delta)
73
+ if delta and getattr(delta, "content", None):
74
+ print(delta.content, end="", flush=True)
75
+ print(" ")
76
+
77
+
78
+
79
+
80
+
VideoAgent/_llm/tokenizer_model.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import os
4
+ class Qwen3TokenizerClient:
5
+ def __init__(self, base_url :str = None):
6
+ self.base_url = base_url or os.getenv("Tokenizer_API_BASE_URL", "http://localhost:8000/v1")
7
+ self.encode_url = f"{self.base_url}/encode"
8
+ self.decode_url = f"{self.base_url}/decode"
9
+ self.batch_url = f"{self.base_url}/batch_encode"
10
+ self.health_url = f"{self.base_url}/health"
11
+
12
+ def check_health(self):
13
+ """检查服务是否正常运行"""
14
+ try:
15
+ response = requests.get(self.health_url)
16
+ if response.status_code == 200:
17
+ print(f"✅ 服务状态正常: {response.json()}")
18
+ return True
19
+ else:
20
+ print(f"❌ 服务检查失败: {response.status_code}")
21
+ return False
22
+ except Exception as e:
23
+ print(f"❌ 无法连接到服务: {e}")
24
+ return False
25
+
26
+ def encode(self, text, add_special_tokens=True):
27
+ """
28
+ 将文本转换为 Token IDs
29
+ """
30
+ payload = {
31
+ "text": text,
32
+ "add_special_tokens": add_special_tokens
33
+ }
34
+
35
+ response = requests.post(self.encode_url, json=payload)
36
+
37
+ if response.status_code == 200:
38
+ return response.json()
39
+ else:
40
+ print(f"❌ 编码失败: {response.text}")
41
+ return None
42
+
43
+ def batch_encode(self, texts, padding=True, max_length=None):
44
+ """
45
+ 批量发送文本进行编码
46
+ """
47
+ payload = {
48
+ "texts": texts,
49
+ "padding": padding,
50
+ "max_length": max_length
51
+ }
52
+
53
+ response = requests.post(self.batch_url, json=payload)
54
+
55
+ if response.status_code == 200:
56
+ return response.json()
57
+ else:
58
+ print(f"❌ 批量编码失败: {response.text}")
59
+ return None
60
+
61
+ def decode(self, token_ids, skip_special_tokens=True):
62
+ """
63
+ 将 Token IDs 还原为文本
64
+ """
65
+ payload = {
66
+ "token_ids": token_ids,
67
+ "skip_special_tokens": skip_special_tokens
68
+ }
69
+
70
+ response = requests.post(self.decode_url, json=payload)
71
+
72
+ if response.status_code == 200:
73
+ return response.json()
74
+ else:
75
+ print(f"❌ 解码失败: {response.text}")
76
+ return None
77
+
78
+ # --- 主程序入口 ---
79
+ if __name__ == "__main__":
80
+ # 1. 初始化客户端 (如果你的 API 运行在不同端口,请修改这里)
81
+ client = Qwen3TokenizerClient(base_url="http://127.0.0.1:8001")
82
+
83
+ # 2. 检查服务状态
84
+ if not client.check_health():
85
+ print("请先启动 API 服务 (python tokenizer_api.py)")
86
+ exit()
87
+
88
+ print("-" * 30)
89
+
90
+ # 3. 测试 Encode (文本 -> ID)
91
+ input_text = "你好,Qwen3!"
92
+ print(f"📝 原始文本: {input_text}")
93
+
94
+ encode_result = client.encode(input_text)
95
+
96
+ if encode_result:
97
+ token_ids = encode_result['token_ids']
98
+ count = encode_result['count']
99
+
100
+ print(f"🔢 Token IDs: {token_ids}")
101
+ print(f"📏 Token 长度: {count}")
102
+
103
+ print("-" * 30)
104
+
105
+ # 4. 测试 Decode (ID -> 文本)
106
+ # 这里我们尝试还原刚才生成的 IDs
107
+ decode_result = client.decode(token_ids, skip_special_tokens=False)
108
+
109
+ if decode_result:
110
+ restored_text = decode_result['text']
111
+ print(f"🔄 还原结果 (含特殊标记): {restored_text}")
112
+
113
+ # 尝试跳过特殊标记还原
114
+ clean_result = client.decode(token_ids, skip_special_tokens=True)
115
+ print(f"✨ 纯净文本: {clean_result['text']}")
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4
3
+ size 11422654
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/tokenizer_config.json ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ },
181
+ "151665": {
182
+ "content": "<tool_response>",
183
+ "lstrip": false,
184
+ "normalized": false,
185
+ "rstrip": false,
186
+ "single_word": false,
187
+ "special": false
188
+ },
189
+ "151666": {
190
+ "content": "</tool_response>",
191
+ "lstrip": false,
192
+ "normalized": false,
193
+ "rstrip": false,
194
+ "single_word": false,
195
+ "special": false
196
+ },
197
+ "151667": {
198
+ "content": "<think>",
199
+ "lstrip": false,
200
+ "normalized": false,
201
+ "rstrip": false,
202
+ "single_word": false,
203
+ "special": false
204
+ },
205
+ "151668": {
206
+ "content": "</think>",
207
+ "lstrip": false,
208
+ "normalized": false,
209
+ "rstrip": false,
210
+ "single_word": false,
211
+ "special": false
212
+ }
213
+ },
214
+ "additional_special_tokens": [
215
+ "<|im_start|>",
216
+ "<|im_end|>",
217
+ "<|object_ref_start|>",
218
+ "<|object_ref_end|>",
219
+ "<|box_start|>",
220
+ "<|box_end|>",
221
+ "<|quad_start|>",
222
+ "<|quad_end|>",
223
+ "<|vision_start|>",
224
+ "<|vision_end|>",
225
+ "<|vision_pad|>",
226
+ "<|image_pad|>",
227
+ "<|video_pad|>"
228
+ ],
229
+ "bos_token": null,
230
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if message.content is string %}\n {%- set content = message.content %}\n {%- else %}\n {%- set content = '' %}\n {%- endif %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}\n{%- endif %}",
231
+ "clean_up_tokenization_spaces": false,
232
+ "eos_token": "<|im_end|>",
233
+ "errors": "replace",
234
+ "model_max_length": 131072,
235
+ "pad_token": "<|endoftext|>",
236
+ "split_special_tokens": false,
237
+ "tokenizer_class": "Qwen2Tokenizer",
238
+ "unk_token": null
239
+ }
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-1.7B/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/.mdl ADDED
Binary file (50 Bytes). View file
 
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/.msc ADDED
Binary file (265 Bytes). View file
 
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4
3
+ size 11422654
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/tokenizer_config.json ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "added_tokens_decoder": {
4
+ "151643": {
5
+ "content": "<|endoftext|>",
6
+ "lstrip": false,
7
+ "normalized": false,
8
+ "rstrip": false,
9
+ "single_word": false,
10
+ "special": true
11
+ },
12
+ "151644": {
13
+ "content": "<|im_start|>",
14
+ "lstrip": false,
15
+ "normalized": false,
16
+ "rstrip": false,
17
+ "single_word": false,
18
+ "special": true
19
+ },
20
+ "151645": {
21
+ "content": "<|im_end|>",
22
+ "lstrip": false,
23
+ "normalized": false,
24
+ "rstrip": false,
25
+ "single_word": false,
26
+ "special": true
27
+ },
28
+ "151646": {
29
+ "content": "<|object_ref_start|>",
30
+ "lstrip": false,
31
+ "normalized": false,
32
+ "rstrip": false,
33
+ "single_word": false,
34
+ "special": true
35
+ },
36
+ "151647": {
37
+ "content": "<|object_ref_end|>",
38
+ "lstrip": false,
39
+ "normalized": false,
40
+ "rstrip": false,
41
+ "single_word": false,
42
+ "special": true
43
+ },
44
+ "151648": {
45
+ "content": "<|box_start|>",
46
+ "lstrip": false,
47
+ "normalized": false,
48
+ "rstrip": false,
49
+ "single_word": false,
50
+ "special": true
51
+ },
52
+ "151649": {
53
+ "content": "<|box_end|>",
54
+ "lstrip": false,
55
+ "normalized": false,
56
+ "rstrip": false,
57
+ "single_word": false,
58
+ "special": true
59
+ },
60
+ "151650": {
61
+ "content": "<|quad_start|>",
62
+ "lstrip": false,
63
+ "normalized": false,
64
+ "rstrip": false,
65
+ "single_word": false,
66
+ "special": true
67
+ },
68
+ "151651": {
69
+ "content": "<|quad_end|>",
70
+ "lstrip": false,
71
+ "normalized": false,
72
+ "rstrip": false,
73
+ "single_word": false,
74
+ "special": true
75
+ },
76
+ "151652": {
77
+ "content": "<|vision_start|>",
78
+ "lstrip": false,
79
+ "normalized": false,
80
+ "rstrip": false,
81
+ "single_word": false,
82
+ "special": true
83
+ },
84
+ "151653": {
85
+ "content": "<|vision_end|>",
86
+ "lstrip": false,
87
+ "normalized": false,
88
+ "rstrip": false,
89
+ "single_word": false,
90
+ "special": true
91
+ },
92
+ "151654": {
93
+ "content": "<|vision_pad|>",
94
+ "lstrip": false,
95
+ "normalized": false,
96
+ "rstrip": false,
97
+ "single_word": false,
98
+ "special": true
99
+ },
100
+ "151655": {
101
+ "content": "<|image_pad|>",
102
+ "lstrip": false,
103
+ "normalized": false,
104
+ "rstrip": false,
105
+ "single_word": false,
106
+ "special": true
107
+ },
108
+ "151656": {
109
+ "content": "<|video_pad|>",
110
+ "lstrip": false,
111
+ "normalized": false,
112
+ "rstrip": false,
113
+ "single_word": false,
114
+ "special": true
115
+ },
116
+ "151657": {
117
+ "content": "<tool_call>",
118
+ "lstrip": false,
119
+ "normalized": false,
120
+ "rstrip": false,
121
+ "single_word": false,
122
+ "special": false
123
+ },
124
+ "151658": {
125
+ "content": "</tool_call>",
126
+ "lstrip": false,
127
+ "normalized": false,
128
+ "rstrip": false,
129
+ "single_word": false,
130
+ "special": false
131
+ },
132
+ "151659": {
133
+ "content": "<|fim_prefix|>",
134
+ "lstrip": false,
135
+ "normalized": false,
136
+ "rstrip": false,
137
+ "single_word": false,
138
+ "special": false
139
+ },
140
+ "151660": {
141
+ "content": "<|fim_middle|>",
142
+ "lstrip": false,
143
+ "normalized": false,
144
+ "rstrip": false,
145
+ "single_word": false,
146
+ "special": false
147
+ },
148
+ "151661": {
149
+ "content": "<|fim_suffix|>",
150
+ "lstrip": false,
151
+ "normalized": false,
152
+ "rstrip": false,
153
+ "single_word": false,
154
+ "special": false
155
+ },
156
+ "151662": {
157
+ "content": "<|fim_pad|>",
158
+ "lstrip": false,
159
+ "normalized": false,
160
+ "rstrip": false,
161
+ "single_word": false,
162
+ "special": false
163
+ },
164
+ "151663": {
165
+ "content": "<|repo_name|>",
166
+ "lstrip": false,
167
+ "normalized": false,
168
+ "rstrip": false,
169
+ "single_word": false,
170
+ "special": false
171
+ },
172
+ "151664": {
173
+ "content": "<|file_sep|>",
174
+ "lstrip": false,
175
+ "normalized": false,
176
+ "rstrip": false,
177
+ "single_word": false,
178
+ "special": false
179
+ },
180
+ "151665": {
181
+ "content": "<tool_response>",
182
+ "lstrip": false,
183
+ "normalized": false,
184
+ "rstrip": false,
185
+ "single_word": false,
186
+ "special": false
187
+ },
188
+ "151666": {
189
+ "content": "</tool_response>",
190
+ "lstrip": false,
191
+ "normalized": false,
192
+ "rstrip": false,
193
+ "single_word": false,
194
+ "special": false
195
+ },
196
+ "151667": {
197
+ "content": "<think>",
198
+ "lstrip": false,
199
+ "normalized": false,
200
+ "rstrip": false,
201
+ "single_word": false,
202
+ "special": false
203
+ },
204
+ "151668": {
205
+ "content": "</think>",
206
+ "lstrip": false,
207
+ "normalized": false,
208
+ "rstrip": false,
209
+ "single_word": false,
210
+ "special": false
211
+ }
212
+ },
213
+ "additional_special_tokens": [
214
+ "<|im_start|>",
215
+ "<|im_end|>",
216
+ "<|object_ref_start|>",
217
+ "<|object_ref_end|>",
218
+ "<|box_start|>",
219
+ "<|box_end|>",
220
+ "<|quad_start|>",
221
+ "<|quad_end|>",
222
+ "<|vision_start|>",
223
+ "<|vision_end|>",
224
+ "<|vision_pad|>",
225
+ "<|image_pad|>",
226
+ "<|video_pad|>"
227
+ ],
228
+ "bos_token": null,
229
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if message.content is string %}\n {%- set content = message.content %}\n {%- else %}\n {%- set content = '' %}\n {%- endif %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}",
230
+ "clean_up_tokenization_spaces": false,
231
+ "eos_token": "<|im_end|>",
232
+ "errors": "replace",
233
+ "model_max_length": 1010000,
234
+ "pad_token": "<|endoftext|>",
235
+ "split_special_tokens": false,
236
+ "tokenizer_class": "Qwen2Tokenizer",
237
+ "unk_token": null,
238
+ "add_bos_token": false
239
+ }
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B-Instruct-2507/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/.mdl ADDED
Binary file (36 Bytes). View file
 
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/.msc ADDED
Binary file (265 Bytes). View file
 
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4
3
+ size 11422654
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/tokenizer_config.json ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ },
181
+ "151665": {
182
+ "content": "<tool_response>",
183
+ "lstrip": false,
184
+ "normalized": false,
185
+ "rstrip": false,
186
+ "single_word": false,
187
+ "special": false
188
+ },
189
+ "151666": {
190
+ "content": "</tool_response>",
191
+ "lstrip": false,
192
+ "normalized": false,
193
+ "rstrip": false,
194
+ "single_word": false,
195
+ "special": false
196
+ },
197
+ "151667": {
198
+ "content": "<think>",
199
+ "lstrip": false,
200
+ "normalized": false,
201
+ "rstrip": false,
202
+ "single_word": false,
203
+ "special": false
204
+ },
205
+ "151668": {
206
+ "content": "</think>",
207
+ "lstrip": false,
208
+ "normalized": false,
209
+ "rstrip": false,
210
+ "single_word": false,
211
+ "special": false
212
+ }
213
+ },
214
+ "additional_special_tokens": [
215
+ "<|im_start|>",
216
+ "<|im_end|>",
217
+ "<|object_ref_start|>",
218
+ "<|object_ref_end|>",
219
+ "<|box_start|>",
220
+ "<|box_end|>",
221
+ "<|quad_start|>",
222
+ "<|quad_end|>",
223
+ "<|vision_start|>",
224
+ "<|vision_end|>",
225
+ "<|vision_pad|>",
226
+ "<|image_pad|>",
227
+ "<|video_pad|>"
228
+ ],
229
+ "bos_token": null,
230
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if message.content is string %}\n {%- set content = message.content %}\n {%- else %}\n {%- set content = '' %}\n {%- endif %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}\n{%- endif %}",
231
+ "clean_up_tokenization_spaces": false,
232
+ "eos_token": "<|im_end|>",
233
+ "errors": "replace",
234
+ "model_max_length": 131072,
235
+ "pad_token": "<|endoftext|>",
236
+ "split_special_tokens": false,
237
+ "tokenizer_class": "Qwen2Tokenizer",
238
+ "unk_token": null
239
+ }
VideoAgent/_llm/tokenizer_model/Qwen/Qwen3-4B/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
VideoAgent/_llm/vlm_model.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ from io import BytesIO
4
+ from PIL import Image
5
+ from openai import OpenAI
6
+
7
+ from .._utils import _pil_to_base64
8
+
9
+
10
+
11
+ class Qwen3VLModel:
12
+ def __init__(self, model_name: str = None, base_url: str = None, api_key: str = None, **kwargs):
13
+ self.model_name = model_name or os.getenv("VLM_MODEL_NAME", "Qwen3-VL-2B-Instruct")
14
+ base_url = base_url or os.getenv("VLM_API_BASE_URL", "http://localhost:8001/v1")
15
+
16
+ api_key = api_key or os.getenv("VLM_API_KEY", "EMPTY")
17
+ self.client = OpenAI(base_url=base_url, api_key=api_key)
18
+
19
+
20
+
21
+ def generate_result(
22
+ self,
23
+ messages,
24
+ max_new_tokens=1024,
25
+ temperature=0.7,
26
+ top_p=0.9,
27
+ **kwargs
28
+ ) -> str:
29
+
30
+
31
+ response = self.client.chat.completions.create(
32
+ model=self.model_name,
33
+ messages=messages,
34
+ max_tokens=max_new_tokens,
35
+ temperature=temperature,
36
+ top_p=top_p,
37
+ )
38
+ return response.choices[0].message.content
39
+
40
+ def generate_result_stream(self, messages, max_new_tokens: int = 1024) -> str:
41
+ response = self.client.chat.completions.create(
42
+ model=self.model_name,
43
+ messages=messages,
44
+ max_tokens=max_new_tokens,
45
+ stream=True,
46
+ )
47
+ result = ""
48
+ for chunk in response:
49
+ delta = chunk.choices[0].delta
50
+ if delta.content:
51
+ print(delta.content, end="", flush=True)
52
+ result += delta.content
53
+ print()
54
+ return result
VideoAgent/_server/embedding_server.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import logging
4
+ import torch
5
+ from typing import Optional, List, Dict, Any, Union
6
+ from fastapi import FastAPI, HTTPException, Form
7
+ from pydantic import BaseModel, Field
8
+ from dotenv import load_dotenv
9
+ from enum import Enum
10
+
11
+ from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage
12
+ from openai.types.embedding import Embedding
13
+
14
+ import os
15
+ import torch
16
+ import torch.nn.functional as F
17
+ import unicodedata
18
+ import numpy as np
19
+ import logging
20
+
21
+ from PIL import Image
22
+ from urllib.parse import urlparse
23
+ from dataclasses import dataclass
24
+ from typing import Optional, List, Union, Dict, Any
25
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLPreTrainedModel, Qwen3VLModel, Qwen3VLConfig
26
+ from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
27
+ from transformers.modeling_outputs import ModelOutput
28
+ from transformers.processing_utils import Unpack
29
+ from transformers.utils import TransformersKwargs
30
+ from transformers.cache_utils import Cache
31
+ from transformers.utils.generic import check_model_inputs
32
+ from qwen_vl_utils.vision_process import process_vision_info
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # Constants for configuration
37
+ MAX_LENGTH = 2048
38
+ IMAGE_BASE_FACTOR = 16
39
+ IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
40
+ MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
41
+ MAX_PIXELS = 1800 * IMAGE_FACTOR * IMAGE_FACTOR
42
+ FPS = 1
43
+ MAX_FRAMES = 64
44
+ FRAME_MAX_PIXELS = 768 * IMAGE_FACTOR * IMAGE_FACTOR
45
+ MAX_TOTAL_PIXELS = 10 * FRAME_MAX_PIXELS
46
+ PAD_TOKEN = "<|endoftext|>"
47
+
48
+ # Define output structure for embeddings
49
+ @dataclass
50
+ class Qwen3VLForEmbeddingOutput(ModelOutput):
51
+ last_hidden_state: Optional[torch.FloatTensor] = None
52
+ attention_mask: Optional[torch.Tensor] = None
53
+
54
+ # Define model class to compute embeddings
55
+ class Qwen3VLForEmbedding(Qwen3VLPreTrainedModel):
56
+ _checkpoint_conversion_mapping = {}
57
+ accepts_loss_kwargs = False
58
+ config: Qwen3VLConfig
59
+
60
+ def __init__(self, config):
61
+ super().__init__(config)
62
+ self.model = Qwen3VLModel(config)
63
+ self.post_init()
64
+
65
+ def get_input_embeddings(self):
66
+ return self.model.get_input_embeddings()
67
+
68
+ def set_input_embeddings(self, value):
69
+ self.model.set_input_embeddings(value)
70
+
71
+ def set_decoder(self, decoder):
72
+ self.model.set_decoder(decoder)
73
+
74
+ def get_decoder(self):
75
+ return self.model.get_decoder()
76
+
77
+ # Extract video features from model
78
+ def get_video_features(self, pixel_values_videos: torch.FloatTensor,
79
+ video_grid_thw: Optional[torch.LongTensor] = None):
80
+ return self.model.get_video_features(pixel_values_videos, video_grid_thw)
81
+
82
+ # Extract image features from model
83
+ def get_image_features(self, pixel_values: torch.FloatTensor,
84
+ image_grid_thw: Optional[torch.LongTensor] = None):
85
+ return self.model.get_image_features(pixel_values, image_grid_thw)
86
+
87
+ # Make modules accessible through properties
88
+ @property
89
+ def language_model(self):
90
+ return self.model.language_model
91
+
92
+ @property
93
+ def visual(self):
94
+ return self.model.visual
95
+
96
+ # Forward pass through model with input parameters
97
+ # @check_model_inputs
98
+ def forward(self,
99
+ input_ids: torch.LongTensor = None,
100
+ attention_mask: Optional[torch.Tensor] = None,
101
+ position_ids: Optional[torch.LongTensor] = None,
102
+ past_key_values: Optional[Cache] = None,
103
+ inputs_embeds: Optional[torch.FloatTensor] = None,
104
+ pixel_values: Optional[torch.Tensor] = None,
105
+ pixel_values_videos: Optional[torch.FloatTensor] = None,
106
+ image_grid_thw: Optional[torch.LongTensor] = None,
107
+ video_grid_thw: Optional[torch.LongTensor] = None,
108
+ cache_position: Optional[torch.LongTensor] = None,
109
+ logits_to_keep: Union[int, torch.Tensor] = 0,
110
+ **kwargs: Unpack[TransformersKwargs],
111
+ ) -> Union[tuple, Qwen3VLForEmbeddingOutput]:
112
+ # Pass inputs through the model
113
+ outputs = self.model(
114
+ input_ids=input_ids,
115
+ pixel_values=pixel_values,
116
+ pixel_values_videos=pixel_values_videos,
117
+ image_grid_thw=image_grid_thw,
118
+ video_grid_thw=video_grid_thw,
119
+ position_ids=position_ids,
120
+ attention_mask=attention_mask,
121
+ past_key_values=past_key_values,
122
+ inputs_embeds=inputs_embeds,
123
+ cache_position=cache_position,
124
+ **kwargs,
125
+ )
126
+ # Return the model output
127
+ return Qwen3VLForEmbeddingOutput(
128
+ last_hidden_state=outputs.last_hidden_state,
129
+ attention_mask=attention_mask,
130
+ )
131
+
132
+
133
+
134
+
135
+
136
+ # Define embedder class for processing inputs and generating embeddings
137
+ class Qwen3VLEmbedder():
138
+ def __init__(
139
+ self,
140
+ model_name_or_path: str,
141
+ max_length: int = MAX_LENGTH,
142
+ min_pixels: int = MIN_PIXELS,
143
+ max_pixels: int = MAX_PIXELS,
144
+ total_pixels: int = MAX_TOTAL_PIXELS,
145
+ fps: float = FPS,
146
+ max_frames: int = MAX_FRAMES,
147
+ default_instruction: str = "Represent the user's input.",
148
+ **kwargs
149
+ ):
150
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
151
+
152
+ self.max_length = max_length
153
+ self.min_pixels = min_pixels
154
+ self.max_pixels = max_pixels
155
+ self.total_pixels = total_pixels
156
+ self.fps = fps
157
+ self.max_frames = max_frames
158
+
159
+ self.default_instruction = default_instruction
160
+
161
+ self.model = Qwen3VLForEmbedding.from_pretrained(
162
+ model_name_or_path, trust_remote_code=True, **kwargs
163
+ ).to(device)
164
+ self.processor = Qwen3VLProcessor.from_pretrained(
165
+ model_name_or_path, padding_side='right'
166
+ )
167
+ self.model.eval()
168
+
169
+ @torch.no_grad()
170
+ def forward(self, inputs: Dict[str, Any]) -> Dict[str, torch.Tensor]:
171
+ outputs = self.model(**inputs)
172
+ return {
173
+ 'last_hidden_state': outputs.last_hidden_state,
174
+ 'attention_mask': inputs.get('attention_mask')
175
+ }
176
+
177
+
178
+ # Preprocess input conversations for model consumption
179
+ def _preprocess_inputs(self, conversations: List[List[Dict]]) -> Dict[str, torch.Tensor]:
180
+ text = self.processor.apply_chat_template(
181
+ conversations, add_generation_prompt=True, tokenize=False
182
+ )
183
+
184
+ try:
185
+ images, video_inputs, video_kwargs = process_vision_info(
186
+ conversations, image_patch_size=16,
187
+ return_video_metadata=True, return_video_kwargs=True
188
+ )
189
+ except Exception as e:
190
+ logger.error(f"Error in processing vision info: {e}")
191
+ images = None
192
+ video_inputs = None
193
+ video_kwargs = {'do_sample_frames': False}
194
+ text = self.processor.apply_chat_template(
195
+ [{'role': 'user', 'content': [{'type': 'text', 'text': 'NULL'}]}],
196
+ add_generation_prompt=True, tokenize=False
197
+ )
198
+
199
+ if video_inputs is not None:
200
+ videos, video_metadata = zip(*video_inputs)
201
+ videos = list(videos)
202
+ video_metadata = list(video_metadata)
203
+ else:
204
+ videos, video_metadata = None, None
205
+
206
+ inputs = self.processor(
207
+ text=text, images=images, videos=videos, video_metadata=video_metadata, truncation=True,
208
+ max_length=self.max_length, padding=True, do_resize=False, return_tensors='pt',
209
+ **video_kwargs
210
+ )
211
+ return inputs
212
+
213
+ # Pool the last hidden state by attention mask for embeddings
214
+ @staticmethod
215
+ def _pooling_last(hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
216
+ flipped_tensor = attention_mask.flip(dims=[1])
217
+ last_one_positions = flipped_tensor.argmax(dim=1)
218
+ col = attention_mask.shape[1] - last_one_positions - 1
219
+ row = torch.arange(hidden_state.shape[0], device=hidden_state.device)
220
+ return hidden_state[row, col]
221
+
222
+ # Process inputs to generate normalized embeddings
223
+ def process(self, inputs: List[List[Dict]], normalize: bool = True) -> tuple:
224
+
225
+
226
+ # print("conversations:\n", inputs)
227
+
228
+ processed_inputs = self._preprocess_inputs(inputs)
229
+ processed_inputs = {k: v.to(self.model.device) for k, v in processed_inputs.items()}
230
+
231
+ outputs = self.forward(processed_inputs)
232
+ embeddings = self._pooling_last(outputs['last_hidden_state'], outputs['attention_mask'])
233
+
234
+ # Normalize the embeddings if specified
235
+ if normalize:
236
+ embeddings = F.normalize(embeddings, p=2, dim=-1)
237
+
238
+ return embeddings
239
+
240
+
241
+
242
+ class EmbeddingRequest(BaseModel):
243
+ messages: List[Dict[str, Any]] = Field(..., description="输入文本或文本列表")
244
+ model: str = Field(default="Qwen3VL", description="模型名称")
245
+ encoding_format: str = Field(default="float", description="输出格式")
246
+
247
+ continue_final_message: Optional[bool] = Field(default=False, description="是否继续生成最终消息")
248
+ add_special_tokens: Optional[bool] = Field(default=False, description="是否添加特殊标记")
249
+
250
+
251
+
252
+
253
+ load_dotenv()
254
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
255
+ logger = logging.getLogger(__name__)
256
+
257
+ app = FastAPI(
258
+ title="Qwen3VL Embedding API",
259
+ description="API for Qwen3VL Embedding model",
260
+ version="1.0.0"
261
+ )
262
+
263
+ # 加载模型
264
+ logger.info("Loading Qwen3VL Embedding model...")
265
+ try:
266
+
267
+
268
+ model_path = os.getenv("EMBEDDING_MODEL_PATH", "")
269
+ embedding_model = Qwen3VLEmbedder(
270
+ model_name_or_path=model_path,
271
+ max_length=2048
272
+ )
273
+ logger.info("Qwen3VL Embedding model loaded successfully!")
274
+ except Exception as e:
275
+ logger.error(f"Failed to load model: {e}")
276
+ embedding_model = None
277
+
278
+
279
+
280
+ def wrap_embedding_list(embedding_list: List[float], index: int = 0) -> Embedding:
281
+ """
282
+ 将embedding列表包装成Embedding类的实例
283
+
284
+ Args:
285
+ embedding_list: 包含浮点数值的列表,表示嵌入向量
286
+ index: 在嵌入列表中的索引
287
+
288
+ Returns:
289
+ Embedding类的实例
290
+ """
291
+ return Embedding(
292
+ embedding=embedding_list,
293
+ index=index,
294
+ object="embedding"
295
+ )
296
+
297
+
298
+ def wrap_multiple_embedding_lists(embedding_lists: List[List[float]]) -> List[Embedding]:
299
+ """
300
+ 将多个embedding列表包装成Embedding类的实例列表
301
+
302
+ Args:
303
+ embedding_lists: 包含多个嵌入向量列表的列表
304
+
305
+ Returns:
306
+ Embedding类实例的列表
307
+ """
308
+ return [wrap_embedding_list(embedding_list, idx) for idx, embedding_list in enumerate(embedding_lists)]
309
+
310
+
311
+
312
+
313
+ @app.post("/v1/embeddings", response_model=CreateEmbeddingResponse)
314
+ async def create_embeddings(request: EmbeddingRequest):
315
+ """
316
+ OpenAI 兼容的 Embeddings 接口
317
+ """
318
+ try:
319
+ if embedding_model is None:
320
+ raise HTTPException(status_code=500, detail="模型未正确加载")
321
+
322
+ conversation = request.messages
323
+
324
+ embedding_result = embedding_model.process(conversation, normalize=True)
325
+ embedding_list = embedding_result.cpu().tolist()
326
+ embedding_objects = wrap_multiple_embedding_lists(embedding_list)
327
+
328
+ return CreateEmbeddingResponse(
329
+ data = embedding_objects,
330
+ model = request.model,
331
+ object = "list",
332
+ usage = Usage(
333
+ prompt_tokens = len(request.messages),
334
+ total_tokens = len(request.messages)
335
+ )
336
+
337
+ )
338
+
339
+ except Exception as e:
340
+ logger.error(f"Error during embedding: {e}")
341
+ raise HTTPException(status_code=500, detail=str(e))
342
+
343
+
344
+ if __name__ == "__main__":
345
+ import uvicorn
346
+ port = int(os.getenv("EMBEDDING_API_PORT", 8006))
347
+ uvicorn.run(app, host="0.0.0.0", port=port)
VideoAgent/_server/llm_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import sys
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+ MODEL_PATH = os.getenv("LLM_MODEL_PATH", "")
8
+ MODEL_NAME = os.getenv("LLM_MODEL_NAME", "")
9
+ GPU_DEVICE_ID = os.getenv("CUDA_VISIBLE_DEVICES", "1") # Default to GPU 0 if not specified
10
+ PORT = os.getenv("LLM_API_PORT", 8009)
11
+ def start_llm_server():
12
+ """
13
+ Start the vLLM server with Qwen3 model on specified GPU
14
+ """
15
+ # Set CUDA_VISIBLE_DEVICES to specify which GPU to use
16
+ env = os.environ.copy()
17
+ env["CUDA_VISIBLE_DEVICES"] = GPU_DEVICE_ID
18
+
19
+ cmd = [
20
+ "vllm", "serve", MODEL_PATH,
21
+ "--trust-remote-code",
22
+ "--dtype", "half",
23
+ "--port", str(PORT),
24
+ "--max-model-len", "4096",
25
+ "--served-model-name", MODEL_NAME,
26
+ "--gpu-memory-utilization", "0.5",
27
+ # "--max-num-batched-tokens", "1024"
28
+ ]
29
+
30
+ print(f"Starting vLLM server on GPU {GPU_DEVICE_ID}")
31
+ # print("Command:", " ".join(cmd))
32
+
33
+ try:
34
+ # Execute the command with the specified GPU environment
35
+ process = subprocess.Popen(cmd, env=env)
36
+
37
+ # Wait for the process to complete (this will run indefinitely since it's a server)
38
+ process.wait()
39
+ except KeyboardInterrupt:
40
+ print("\nServer stopped by user.")
41
+ process.terminate()
42
+ process.wait()
43
+
44
+ if __name__ == "__main__":
45
+ start_llm_server()
VideoAgent/_server/qwen3_vl_embedding.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn.functional as F
4
+ import unicodedata
5
+ import numpy as np
6
+ import logging
7
+
8
+ from PIL import Image
9
+ from urllib.parse import urlparse
10
+ from dataclasses import dataclass
11
+ from typing import Optional, List, Union, Dict, Any
12
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLPreTrainedModel, Qwen3VLModel, Qwen3VLConfig
13
+ from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
14
+ from transformers.modeling_outputs import ModelOutput
15
+ from transformers.processing_utils import Unpack
16
+ from transformers.utils import TransformersKwargs
17
+ from transformers.cache_utils import Cache
18
+ from transformers.utils.generic import check_model_inputs
19
+ from qwen_vl_utils.vision_process import process_vision_info
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Constants for configuration
24
+ MAX_LENGTH = 2048
25
+ IMAGE_BASE_FACTOR = 16
26
+ IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
27
+ MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
28
+ MAX_PIXELS = 1800 * IMAGE_FACTOR * IMAGE_FACTOR
29
+ FPS = 1
30
+ MAX_FRAMES = 64
31
+ FRAME_MAX_PIXELS = 768 * IMAGE_FACTOR * IMAGE_FACTOR
32
+ MAX_TOTAL_PIXELS = 10 * FRAME_MAX_PIXELS
33
+ PAD_TOKEN = "<|endoftext|>"
34
+
35
+ # Define output structure for embeddings
36
+ @dataclass
37
+ class Qwen3VLForEmbeddingOutput(ModelOutput):
38
+ last_hidden_state: Optional[torch.FloatTensor] = None
39
+ attention_mask: Optional[torch.Tensor] = None
40
+
41
+ # Define model class to compute embeddings
42
+ class Qwen3VLForEmbedding(Qwen3VLPreTrainedModel):
43
+ _checkpoint_conversion_mapping = {}
44
+ accepts_loss_kwargs = False
45
+ config: Qwen3VLConfig
46
+
47
+ def __init__(self, config):
48
+ super().__init__(config)
49
+ self.model = Qwen3VLModel(config)
50
+ self.post_init()
51
+
52
+ def get_input_embeddings(self):
53
+ return self.model.get_input_embeddings()
54
+
55
+ def set_input_embeddings(self, value):
56
+ self.model.set_input_embeddings(value)
57
+
58
+ def set_decoder(self, decoder):
59
+ self.model.set_decoder(decoder)
60
+
61
+ def get_decoder(self):
62
+ return self.model.get_decoder()
63
+
64
+ # Extract video features from model
65
+ def get_video_features(self, pixel_values_videos: torch.FloatTensor,
66
+ video_grid_thw: Optional[torch.LongTensor] = None):
67
+ return self.model.get_video_features(pixel_values_videos, video_grid_thw)
68
+
69
+ # Extract image features from model
70
+ def get_image_features(self, pixel_values: torch.FloatTensor,
71
+ image_grid_thw: Optional[torch.LongTensor] = None):
72
+ return self.model.get_image_features(pixel_values, image_grid_thw)
73
+
74
+ # Make modules accessible through properties
75
+ @property
76
+ def language_model(self):
77
+ return self.model.language_model
78
+
79
+ @property
80
+ def visual(self):
81
+ return self.model.visual
82
+
83
+ # Forward pass through model with input parameters
84
+ # @check_model_inputs
85
+ def forward(self,
86
+ input_ids: torch.LongTensor = None,
87
+ attention_mask: Optional[torch.Tensor] = None,
88
+ position_ids: Optional[torch.LongTensor] = None,
89
+ past_key_values: Optional[Cache] = None,
90
+ inputs_embeds: Optional[torch.FloatTensor] = None,
91
+ pixel_values: Optional[torch.Tensor] = None,
92
+ pixel_values_videos: Optional[torch.FloatTensor] = None,
93
+ image_grid_thw: Optional[torch.LongTensor] = None,
94
+ video_grid_thw: Optional[torch.LongTensor] = None,
95
+ cache_position: Optional[torch.LongTensor] = None,
96
+ logits_to_keep: Union[int, torch.Tensor] = 0,
97
+ **kwargs: Unpack[TransformersKwargs],
98
+ ) -> Union[tuple, Qwen3VLForEmbeddingOutput]:
99
+ # Pass inputs through the model
100
+ outputs = self.model(
101
+ input_ids=input_ids,
102
+ pixel_values=pixel_values,
103
+ pixel_values_videos=pixel_values_videos,
104
+ image_grid_thw=image_grid_thw,
105
+ video_grid_thw=video_grid_thw,
106
+ position_ids=position_ids,
107
+ attention_mask=attention_mask,
108
+ past_key_values=past_key_values,
109
+ inputs_embeds=inputs_embeds,
110
+ cache_position=cache_position,
111
+ **kwargs,
112
+ )
113
+ # Return the model output
114
+ return Qwen3VLForEmbeddingOutput(
115
+ last_hidden_state=outputs.last_hidden_state,
116
+ attention_mask=attention_mask,
117
+ )
118
+
119
+ def sample_frames(frames: List[Union[str, Image.Image]], max_segments: int) -> List[Union[str, Image.Image]]:
120
+ duration = len(frames)
121
+ if duration <= max_segments:
122
+ return frames
123
+
124
+ frame_id_array = np.linspace(0, duration - 1, max_segments, dtype=int)
125
+ frame_id_list = frame_id_array.tolist()
126
+ sampled_frames = [ frames[frame_idx] for frame_idx in frame_id_list ]
127
+ return sampled_frames
128
+
129
+ def is_image_path(path: str) -> bool:
130
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
131
+
132
+ if path.startswith(('http://', 'https://')):
133
+ # Parse URL to remove query parameters
134
+ parsed_url = urlparse(path)
135
+ clean_path = parsed_url.path
136
+ else:
137
+ clean_path = path
138
+
139
+ # Check file extension
140
+ _, ext = os.path.splitext(clean_path.lower())
141
+ return ext in image_extensions
142
+
143
+ def is_video_input(video) -> bool:
144
+ if isinstance(video, str):
145
+ return True
146
+
147
+ if isinstance(video, list) and len(video) > 0:
148
+ # Check first element to determine the type
149
+ first_elem = video[0]
150
+
151
+ if isinstance(first_elem, Image.Image):
152
+ return True
153
+
154
+ if isinstance(first_elem, str):
155
+ return is_image_path(first_elem)
156
+
157
+ return False
158
+
159
+ # Define embedder class for processing inputs and generating embeddings
160
+ class Qwen3VLEmbedder():
161
+ def __init__(
162
+ self,
163
+ model_name_or_path: str,
164
+ max_length: int = MAX_LENGTH,
165
+ min_pixels: int = MIN_PIXELS,
166
+ max_pixels: int = MAX_PIXELS,
167
+ total_pixels: int = MAX_TOTAL_PIXELS,
168
+ fps: float = FPS,
169
+ max_frames: int = MAX_FRAMES,
170
+ default_instruction: str = "Represent the user's input.",
171
+ **kwargs
172
+ ):
173
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
174
+
175
+ self.max_length = max_length
176
+ self.min_pixels = min_pixels
177
+ self.max_pixels = max_pixels
178
+ self.total_pixels = total_pixels
179
+ self.fps = fps
180
+ self.max_frames = max_frames
181
+
182
+ self.default_instruction = default_instruction
183
+
184
+ self.model = Qwen3VLForEmbedding.from_pretrained(
185
+ model_name_or_path, trust_remote_code=True, **kwargs
186
+ ).to(device)
187
+ self.processor = Qwen3VLProcessor.from_pretrained(
188
+ model_name_or_path, padding_side='right'
189
+ )
190
+ self.model.eval()
191
+
192
+ @torch.no_grad()
193
+ def forward(self, inputs: Dict[str, Any]) -> Dict[str, torch.Tensor]:
194
+ outputs = self.model(**inputs)
195
+ return {
196
+ 'last_hidden_state': outputs.last_hidden_state,
197
+ 'attention_mask': inputs.get('attention_mask')
198
+ }
199
+
200
+ # Truncate token sequence to a specified max length
201
+ def _truncate_tokens(self, token_ids: List[int], max_length: int) -> List[int]:
202
+ if len(token_ids) <= max_length:
203
+ return token_ids
204
+
205
+ special_token_ids = set(self.processor.tokenizer.all_special_ids)
206
+ num_special = sum(1 for token_idx in token_ids if token_idx in special_token_ids)
207
+ num_non_special_to_keep = max_length - num_special
208
+
209
+ final_token_ids = []
210
+ non_special_kept_count = 0
211
+ # Ensure retention of special tokens while truncating the rest
212
+ for token_idx in token_ids:
213
+ if token_idx in special_token_ids:
214
+ final_token_ids.append(token_idx)
215
+ elif non_special_kept_count < num_non_special_to_keep:
216
+ final_token_ids.append(token_idx)
217
+ non_special_kept_count += 1
218
+ return final_token_ids
219
+
220
+ def format_model_input(
221
+ self,
222
+ text: Optional[Union[List[str], str]] = None,
223
+ image: Optional[Union[List[Union[str, Image.Image]], str, Image.Image]] = None,
224
+ video: Optional[Union[List[Union[str, List[Union[str, Image.Image]]]], str, List[Union[str, Image.Image]]]] = None,
225
+ instruction: Optional[str] = None,
226
+ fps: Optional[float] = None,
227
+ max_frames: Optional[int] = None
228
+ ) -> List[Dict]:
229
+
230
+ # Ensure instruction ends with punctuation
231
+ if instruction:
232
+ instruction = instruction.strip()
233
+ if instruction and not unicodedata.category(instruction[-1]).startswith('P'):
234
+ instruction = instruction + '.'
235
+
236
+ # Initialize conversation with system prompts
237
+ content = []
238
+ conversation = [
239
+ {"role": "system", "content": [{"type": "text", "text": instruction or self.default_instruction}]},
240
+ {"role": "user", "content": content}
241
+ ]
242
+
243
+ # Normalize text input to list
244
+ if text is None:
245
+ texts = []
246
+ elif isinstance(text, str):
247
+ texts = [text]
248
+ else:
249
+ texts = text
250
+
251
+ # Normalize image input to list
252
+ if image is None:
253
+ images = []
254
+ elif not isinstance(image, list):
255
+ images = [image]
256
+ else:
257
+ images = image
258
+
259
+ # Normalize video input to list
260
+ if video is None:
261
+ videos = []
262
+ elif is_video_input(video):
263
+ videos = [video]
264
+ else:
265
+ # Assume it's a list of videos
266
+ videos = video
267
+
268
+ # Add text, image, or video content to conversation
269
+ if not texts and not images and not videos:
270
+ content.append({'type': 'text', 'text': "NULL"})
271
+ return conversation
272
+
273
+ # Process each video
274
+ for vid in videos:
275
+ video_content = None
276
+ video_kwargs = {'total_pixels': self.total_pixels}
277
+
278
+ if isinstance(vid, list):
279
+ # Video as frame sequence
280
+ video_content = vid
281
+ if self.max_frames is not None:
282
+ video_content = sample_frames(video_content, self.max_frames)
283
+ video_content = [
284
+ ('file://' + ele if isinstance(ele, str) else ele)
285
+ for ele in video_content
286
+ ]
287
+ elif isinstance(vid, str):
288
+ # Video as file path
289
+ video_content = vid if vid.startswith(('http://', 'https://')) else 'file://' + vid
290
+ video_kwargs = {'fps': fps or self.fps, 'max_frames': max_frames or self.max_frames}
291
+ else:
292
+ raise TypeError(f"Unrecognized video type: {type(vid)}")
293
+
294
+ # Add video input to content
295
+ if video_content:
296
+ content.append({
297
+ 'type': 'video',
298
+ 'video': video_content,
299
+ **video_kwargs
300
+ })
301
+
302
+ # Process each image
303
+ for img in images:
304
+ image_content = None
305
+
306
+ if isinstance(img, Image.Image):
307
+ image_content = img
308
+ elif isinstance(img, str):
309
+ image_content = img if img.startswith(('http://', 'https://')) else 'file://' + img
310
+ else:
311
+ raise TypeError(f"Unrecognized image type: {type(img)}")
312
+
313
+ # Add image input to content
314
+ if image_content:
315
+ content.append({
316
+ 'type': 'image',
317
+ 'image': image_content,
318
+ "min_pixels": self.min_pixels,
319
+ "max_pixels": self.max_pixels
320
+ })
321
+
322
+ # Process each text
323
+ for txt in texts:
324
+ content.append({'type': 'text', 'text': txt})
325
+
326
+ return conversation
327
+
328
+ # Preprocess input conversations for model consumption
329
+ def _preprocess_inputs(self, conversations: List[List[Dict]]) -> Dict[str, torch.Tensor]:
330
+ text = self.processor.apply_chat_template(
331
+ conversations, add_generation_prompt=True, tokenize=False
332
+ )
333
+
334
+ try:
335
+ images, video_inputs, video_kwargs = process_vision_info(
336
+ conversations, image_patch_size=16,
337
+ return_video_metadata=True, return_video_kwargs=True
338
+ )
339
+ except Exception as e:
340
+ logger.error(f"Error in processing vision info: {e}")
341
+ images = None
342
+ video_inputs = None
343
+ video_kwargs = {'do_sample_frames': False}
344
+ text = self.processor.apply_chat_template(
345
+ [{'role': 'user', 'content': [{'type': 'text', 'text': 'NULL'}]}],
346
+ add_generation_prompt=True, tokenize=False
347
+ )
348
+
349
+ if video_inputs is not None:
350
+ videos, video_metadata = zip(*video_inputs)
351
+ videos = list(videos)
352
+ video_metadata = list(video_metadata)
353
+ else:
354
+ videos, video_metadata = None, None
355
+
356
+ inputs = self.processor(
357
+ text=text, images=images, videos=videos, video_metadata=video_metadata, truncation=True,
358
+ max_length=self.max_length, padding=True, do_resize=False, return_tensors='pt',
359
+ **video_kwargs
360
+ )
361
+ return inputs
362
+
363
+ # Pool the last hidden state by attention mask for embeddings
364
+ @staticmethod
365
+ def _pooling_last(hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
366
+ flipped_tensor = attention_mask.flip(dims=[1])
367
+ last_one_positions = flipped_tensor.argmax(dim=1)
368
+ col = attention_mask.shape[1] - last_one_positions - 1
369
+ row = torch.arange(hidden_state.shape[0], device=hidden_state.device)
370
+ return hidden_state[row, col]
371
+
372
+ # Process inputs to generate normalized embeddings
373
+ def process(self, inputs: List[Dict[str, Any]], normalize: bool = True) -> tuple:
374
+ conversations = [self.format_model_input(
375
+ text=ele.get('text'),
376
+ image=ele.get('image'),
377
+ video=ele.get('video'),
378
+ instruction=ele.get('instruction'),
379
+ fps=ele.get('fps'),
380
+ max_frames=ele.get('max_frames')
381
+ ) for ele in inputs]
382
+
383
+ print("conversations:\n", conversations)
384
+
385
+ processed_inputs = self._preprocess_inputs(conversations)
386
+ processed_inputs = {k: v.to(self.model.device) for k, v in processed_inputs.items()}
387
+
388
+ outputs = self.forward(processed_inputs)
389
+ embeddings = self._pooling_last(outputs['last_hidden_state'], outputs['attention_mask'])
390
+
391
+ # Normalize the embeddings if specified
392
+ if normalize:
393
+ embeddings = F.normalize(embeddings, p=2, dim=-1)
394
+
395
+ return embeddings
396
+
397
+ def count_tokens(self, text=None, image=None, video=None):
398
+ """计算输入的token数量"""
399
+ conversation = self.format_model_input(
400
+ text=text, image=image, video=video
401
+ )
402
+ inputs = self._preprocess_inputs([conversation])
403
+
404
+ # 获取token数量
405
+ token_count = inputs['input_ids'].shape[1]
406
+ print(f"Token count: {token_count}")
407
+ return token_count
VideoAgent/_server/sherpa_asr_server.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import tempfile
5
+ import logging
6
+ import subprocess
7
+ import soundfile as sf
8
+ from typing import Optional, List
9
+ from fastapi import FastAPI, HTTPException, Body, UploadFile, File
10
+ from fastapi.responses import JSONResponse
11
+ import numpy as np
12
+ from contextlib import asynccontextmanager
13
+ from dotenv import load_dotenv
14
+ load_dotenv()
15
+
16
+
17
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # 全局 ASR 实例
22
+ asr_engine = None
23
+
24
+
25
+ class SherpaASREngine:
26
+ """sherpa-onnx-offline 命令行引擎封装"""
27
+
28
+ def __init__(
29
+ self,
30
+ model_dir: str = None,
31
+ model_file: str = None,
32
+ tokens_file: str = None,
33
+ sherpa_bin: str = None,
34
+ vad: str = None,
35
+ provider: str = "axera",
36
+ ):
37
+ base = model_dir or os.getenv("SHERPA_MODEL_DIR", os.path.dirname(os.path.abspath(__file__)))
38
+ self.model_file = model_file or os.getenv("SHERPA_MODEL_FILE", os.path.join(base, "ax650", "model-10-seconds.axmodel"))
39
+ self.tokens_file = tokens_file or os.path.join(base, "tokens.txt")
40
+ self.sherpa_bin = sherpa_bin or os.path.join(
41
+ base,
42
+ os.getenv("SHERPA_BIN_DIR", "sherpa-onnx-v1.12.20-axera-ax650-linux-aarch64-shared"),
43
+ "bin",
44
+ "sherpa-onnx-offline",
45
+ )
46
+ self.provider = provider or os.getenv("SHERPA_PROVIDER", "axera")
47
+ self.vad = vad or os.getenv("vad-model", "/root/huangjie/AXERA-TECH/SenseVoice/silero_vad.onnx")
48
+
49
+ if os.path.exists(self.sherpa_bin):
50
+ os.chmod(self.sherpa_bin, 0o755)
51
+
52
+ def run(self, audio_path: str) -> dict:
53
+ """执行识别命令,返回解析后的 JSON 结果"""
54
+ cmd = [
55
+ self.sherpa_bin,
56
+ # f"--silero-vad-model={self.vad}",
57
+ f"--sense-voice-model={self.model_file}",
58
+ f"--tokens={self.tokens_file}",
59
+ f"--provider={self.provider}",
60
+ audio_path,
61
+ ]
62
+
63
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
64
+
65
+ if result.returncode != 0:
66
+ logger.error(f"sherpa-onnx failed: {result.stderr}")
67
+ raise RuntimeError(f"sherpa-onnx ASR failed: {result.stderr}")
68
+ print("result: ", result)
69
+
70
+
71
+
72
+ # 解析输出中的 JSON 行
73
+ for line in reversed(result.stderr.strip().splitlines()):
74
+ line = line.strip()
75
+ if line.startswith("{"):
76
+ # text=line.json().get("text", "")
77
+ # lang=line.json().get("lang", "")
78
+ print("lang: ", line)
79
+ return json.loads(line)
80
+
81
+ return {"text": "", "lang": "", "timestamps": []}
82
+
83
+
84
+ def clean_text(text: str) -> str:
85
+ """清理文本中的特殊标记"""
86
+ text = re.sub(r'<\|[^|]*\|>', '', text)
87
+ text = re.sub(r'\s+', ' ', text).strip()
88
+ return text
89
+
90
+
91
+ @asynccontextmanager
92
+ async def lifespan(app: FastAPI):
93
+ global asr_engine
94
+ logger.info("Loading Sherpa-ONNX ASR engine...")
95
+ try:
96
+ asr_engine = SherpaASREngine()
97
+ logger.info("Sherpa-ONNX ASR engine loaded successfully")
98
+ except Exception as e:
99
+ logger.error(f"Failed to load Sherpa-ONNX ASR engine: {str(e)}")
100
+ raise
101
+ yield
102
+
103
+
104
+ app = FastAPI(title="Sherpa-ONNX ASR Server", description="SenseVoice ASR via sherpa-onnx-offline", lifespan=lifespan)
105
+
106
+
107
+
108
+ @app.post("/asr", summary="Recognize speech from raw audio data")
109
+ async def recognize_speech(
110
+ audio_data: List[float] = Body(..., embed=True, description="Audio data as list of floats"),
111
+ sample_rate: Optional[int] = Body(16000, description="Audio sample rate in Hz"),
112
+ ):
113
+ """接收 numpy 数组格式的音频数据并返回识别结果"""
114
+ if asr_engine is None:
115
+ raise HTTPException(status_code=503, detail="ASR engine not loaded")
116
+
117
+ try:
118
+ np_audio = np.array(audio_data, dtype=np.float32)
119
+ if np_audio.ndim != 1 or len(np_audio) == 0:
120
+ raise HTTPException(status_code=400, detail="Audio data must be a non-empty 1D array")
121
+
122
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
123
+ tmp_path = tmp.name
124
+ sf.write(tmp_path, np_audio, sample_rate)
125
+
126
+ try:
127
+ result = asr_engine.run(tmp_path)
128
+ result["text"] = clean_text(result.get("text", ""))
129
+ return JSONResponse(content=result)
130
+ finally:
131
+ try:
132
+ os.remove(tmp_path)
133
+ except Exception:
134
+ pass
135
+
136
+ except HTTPException:
137
+ raise
138
+ except Exception as e:
139
+ logger.error(f"Recognition error: {str(e)}")
140
+ raise HTTPException(status_code=500, detail=str(e))
141
+
142
+
143
+ @app.post("/asr/file", summary="Recognize speech from uploaded audio file")
144
+ async def recognize_file(file: UploadFile = File(..., description="Audio file (wav, mp3, etc.)")):
145
+ """接收音频文件并返回识别结果"""
146
+ if asr_engine is None:
147
+ raise HTTPException(status_code=503, detail="ASR engine not loaded")
148
+
149
+ try:
150
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
151
+ tmp_path = tmp.name
152
+ content = await file.read()
153
+ tmp.write(content)
154
+
155
+ try:
156
+ result = asr_engine.run(tmp_path)
157
+ result["text"] = clean_text(result.get("text", ""))
158
+ return JSONResponse(content=result)
159
+ finally:
160
+ try:
161
+ os.remove(tmp_path)
162
+ except Exception:
163
+ pass
164
+
165
+ except HTTPException:
166
+ raise
167
+ except Exception as e:
168
+ logger.error(f"Recognition error: {str(e)}")
169
+ raise HTTPException(status_code=500, detail=str(e))
170
+
171
+
172
+ @app.get("/health")
173
+ async def health_check():
174
+ return {"status": "ok", "model_loaded": asr_engine is not None}
175
+
176
+
177
+ if __name__ == "__main__":
178
+ import uvicorn
179
+ port = int(os.getenv("SHERPA_ASR_API_PORT", 8006))
180
+ uvicorn.run(app, host="0.0.0.0", port=port)
VideoAgent/_server/tokenizer_server.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel
3
+ from typing import List, Optional
4
+ from transformers import AutoTokenizer
5
+ import uvicorn
6
+ import os
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+ # 1. 初始化 FastAPI
11
+ app = FastAPI(title="Qwen3 Tokenizer API", version="1.0")
12
+
13
+ # 2. 全局加载 Tokenizer (避免每次请求都加载)
14
+ # 如果是本地文件,请替换为本地路径,例如 "./qwen3_tokenizer"
15
+ MODEL_PATH = os.getenv("Tokenizer_MODEL_PATH", "")
16
+ print("MODEL_PATH :",MODEL_PATH )
17
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=False)
18
+
19
+ # 3. 定义请求数据结构
20
+ class EncodeRequest(BaseModel):
21
+ text: str
22
+ add_special_tokens: bool = True # 是否添加 <\|im_start\|> 等特殊标记
23
+
24
+ class DecodeRequest(BaseModel):
25
+ token_ids: List[int]
26
+ skip_special_tokens: bool = True # 是否跳过特殊标记
27
+
28
+ class BatchEncodeRequest(BaseModel):
29
+ texts: List[str] # 接收文本列表
30
+ padding: bool = False # 是否自动填充到最长序列
31
+ truncation: bool = True # 是否截断超长文本
32
+ max_length: Optional[int] = None # 最大长度限制
33
+ add_special_tokens: bool = True
34
+
35
+ # 4. 定义 API 路由
36
+
37
+ @app.get("/health")
38
+ async def health_check():
39
+ """健康检查接口"""
40
+ return {"status": "running", "model": tokenizer.name_or_path}
41
+
42
+ @app.post("/encode")
43
+ async def encode_text(request: EncodeRequest):
44
+ """
45
+ 将文本转换为 Token IDs
46
+ """
47
+ try:
48
+ # 调用 tokenizer
49
+ encoding = tokenizer(
50
+ request.text,
51
+ add_special_tokens=request.add_special_tokens,
52
+ return_tensors="pt" # 返回 PyTorch 张量,也可以选 "np"
53
+ )
54
+
55
+ token_ids = encoding.input_ids[0].tolist()
56
+
57
+ return {
58
+ "text": request.text,
59
+ "token_ids": token_ids,
60
+ "count": len(token_ids), # 返回长度,方便前端计算
61
+ "special_tokens_added": request.add_special_tokens
62
+ }
63
+ except Exception as e:
64
+ raise HTTPException(status_code=500, detail=str(e))
65
+
66
+ @app.post("/batch_encode")
67
+ async def batch_encode(request: BatchEncodeRequest):
68
+ """
69
+ 批量将文本列表转换为 Token IDs
70
+ """
71
+ try:
72
+ # 调用 tokenizer,直接传入列表即可实现批量处理
73
+ encoding = tokenizer(
74
+ request.texts,
75
+ padding=request.padding,
76
+ truncation=request.truncation,
77
+ max_length=request.max_length,
78
+ add_special_tokens=request.add_special_tokens,
79
+ return_tensors="pt"
80
+ )
81
+
82
+ # 将 PyTorch Tensor 转换为 Python 列表以便 JSON 序列化
83
+ # input_ids 的形状通常是 [batch_size, sequence_length]
84
+ batch_token_ids = encoding.input_ids.tolist()
85
+
86
+ # 计算每个文本的长度
87
+ lengths = [len(tokens) for tokens in batch_token_ids]
88
+
89
+ return {
90
+ "batch_size": len(request.texts),
91
+ "token_ids_batch": batch_token_ids,
92
+ "lengths": lengths
93
+ }
94
+
95
+ except Exception as e:
96
+ raise HTTPException(status_code=500, detail=str(e))
97
+
98
+ @app.post("/decode")
99
+ async def decode_tokens(request: DecodeRequest):
100
+ """
101
+ 将 Token IDs 还原为文本
102
+ """
103
+ try:
104
+ text = tokenizer.decode(
105
+ request.token_ids,
106
+ skip_special_tokens=request.skip_special_tokens
107
+ )
108
+ return {
109
+ "token_ids": request.token_ids,
110
+ "text": text,
111
+ "count": len(request.token_ids)
112
+ }
113
+ except Exception as e:
114
+ raise HTTPException(status_code=500, detail=str(e))
115
+
116
+ # 5. 启动服务
117
+ if __name__ == "__main__":
118
+ # 启动命令:uvicorn tokenizer_api:app --host 0.0.0.0 --port 8001
119
+ import uvicorn
120
+ port = int(os.getenv("Tokenizer_API_PORT", 8007))
121
+ uvicorn.run(app, host="0.0.0.0", port=port)
VideoAgent/_server/vlm_server.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import sys
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+ MODEL_PATH = os.getenv("VLM_MODEL_PATH", "")
8
+ MODEL_NAME = os.getenv("VLM_MODEL_NAME", "")
9
+ GPU_DEVICE_ID = os.getenv("CUDA_VISIBLE_DEVICES", "1") # Default to GPU 0 if not specified
10
+ PORT = os.getenv("VLM_API_PORT", 8008)
11
+ def start_llm_server():
12
+ """
13
+ Start the vLLM server with Qwen3 model on specified GPU
14
+ """
15
+ # Set CUDA_VISIBLE_DEVICES to specify which GPU to use
16
+ env = os.environ.copy()
17
+ env["CUDA_VISIBLE_DEVICES"] = GPU_DEVICE_ID
18
+
19
+ cmd = [
20
+ "vllm", "serve", MODEL_PATH,
21
+ "--trust-remote-code",
22
+ "--dtype", "half",
23
+ "--port", str(PORT),
24
+ "--max-model-len", "4096",
25
+ "--served-model-name", MODEL_NAME,
26
+ "--gpu-memory-utilization", "0.35",
27
+ # "--max-num-batched-tokens", "1024"
28
+ ]
29
+
30
+ print(f"Starting vLLM server on GPU {GPU_DEVICE_ID}")
31
+ # print("Command:", " ".join(cmd))
32
+
33
+ try:
34
+ # Execute the command with the specified GPU environment
35
+ process = subprocess.Popen(cmd, env=env)
36
+
37
+ # Wait for the process to complete (this will run indefinitely since it's a server)
38
+ process.wait()
39
+ except KeyboardInterrupt:
40
+ print("\nServer stopped by user.")
41
+ process.terminate()
42
+ process.wait()
43
+
44
+ if __name__ == "__main__":
45
+ start_llm_server()
VideoAgent/_storage/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .vdb_nanovectordb import NanoVectorDBStorage, NanoVectorDBVideoSegmentStorage
2
+ from .kv_json import JsonKVStorage
VideoAgent/_storage/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (312 Bytes). View file
 
VideoAgent/_storage/__pycache__/kv_json.cpython-310.pyc ADDED
Binary file (2.73 kB). View file