1
0
forked from zbw/yiliao2026
Files
yiliao2026/vlm_server.py
Orange 779b32362a Please enter the commit message for your changes. Lines starting
with '#' will be ignored, and an empty message aborts the commit.

On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
	modified:   .gitignore
	modified:   README.md
	new file:   bashes/auto-wifi-connect.service
	new file:   bashes/auto-wifi-connect.sh
	deleted:    keyboard_control.py
	new file:   my_model/image.png
	new file:   path_follower_demo.py
	new file:   scripts/PIDtracking.py
	new file:   scripts/__pycache__/publish_sine_path.cpython-310.pyc
	new file:   scripts/publish_sine_path.py
	modified:   src/gc_navigation2_slamtoolbox/params/gc_navigation_slam.yaml
	modified:   src/gc_navigation2_slamtoolbox/params/gc_navigation_slam.yaml.bak
	new file:   src/gc_navigation2_slamtoolbox/params/gc_navigation_slam.yaml.bak2
	modified:   src/origincar_base/config/ekf.yaml
	new file:   src/origincar_base/config/ekf.yaml.bak
	modified:   src/origincar_base/launch/base_serial.launch.py
	new file:   src/origincar_base/launch/base_serial.launch.py.bak
	modified:   src/origincar_base/launch/origincar_bringup.launch.py
	new file:   src/past_control/CMakeLists.txt
	new file:   src/past_control/config/past_control.yaml
	new file:   src/past_control/include/past_control/tools.h
	new file:   src/past_control/launch/past_control.launch.py
	new file:   src/past_control/msg/Obstacle.msg
	new file:   src/past_control/msg/ObstacleArray.msg
	new file:   src/past_control/package.xml
	new file:   src/past_control/src/lane_follower_node.cpp
	new file:   src/past_control/src/obstacle_detector_node.cpp
	new file:   src/past_control/src/racing_orchestrator.cpp
	new file:   src/planner/CMakeLists.txt
	new file:   src/planner/config/planner.yaml
	new file:   src/planner/launch/planner.launch.py
	new file:   src/planner/package.xml
	new file:   src/planner/src/planner_version.cpp
	modified:   src/qr_detection/src/qr_dete_depth.cpp
	new file:   src/racing_control/CMakeLists.txt
	new file:   src/racing_control/include/racing_control/racing_control.hpp
	new file:   src/racing_control/package.xml
	new file:   src/racing_control/src/racing_control.cpp
	modified:   src/vlm_detect/setup.py
	new file:   src/vlm_detect/vlm_detect/__pycache__/__init__.cpython-310.pyc
	new file:   src/vlm_detect/vlm_detect/__pycache__/tts_node.cpython-310.pyc
	new file:   src/vlm_detect/vlm_detect/test_publisher.py
	new file:   src/vlm_detect/vlm_detect/tts_node.py
	modified:   src/vlm_detect/vlm_detect/vlm_node.py
	new file:   tools/measure_turning_radius.py
	new file:   tools/set_volume.py
	new file:   tools/udp_to_cmdvel.py
	new file:   tools/windows_keyboard_control.py
	new file:   vlm_server.py
	new file:   "\350\260\203\350\257\225\350\256\260\345\275\225.Assets/1.png"
	renamed:    "\350\260\203\350\257\225\350\256\260\345\275\225.log" -> "\350\260\203\350\257\225\350\256\260\345\275\225.md"
2026-06-22 17:16:45 +08:00

208 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VLM 图生文推理服务 — AndesVL-1B (千问体系)
为 RDK X5 提供 OpenAI 兼容 API纯 CPU 推理)
启动方式: python vlm_server.py
API 地址: http://192.168.10.210:8000
"""
import base64
import io
import time
import uuid
import logging
import sys
from contextlib import asynccontextmanager
import torch
import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from PIL import Image
from transformers import AutoModel, AutoTokenizer, AutoImageProcessor
# ========== 配置 ==========
MODEL_PATH = "/home/root/models/AndesVL-1B-Instruct" # RDK X5 上模型路径
HOST = "0.0.0.0"
PORT = 8000
# ========== 日志 ==========
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("vlm_server")
# ========== 全局变量 ==========
model = None
tokenizer = None
image_processor = None
def load_model():
"""加载 AndesVL-1B 模型"""
global model, tokenizer, image_processor
logger.info(f"模型路径: {MODEL_PATH}")
logger.info("加载 tokenizer & image processor...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
image_processor = AutoImageProcessor.from_pretrained(
MODEL_PATH, trust_remote_code=True
)
logger.info("加载 AndesVL-1B 模型 (fp32 CPU, 约 18 秒)...")
t0 = time.time()
model = AutoModel.from_pretrained(
MODEL_PATH,
trust_remote_code=True,
torch_dtype=torch.float32,
low_cpu_mem_usage=True,
)
model.eval()
logger.info(f"模型加载完成,耗时 {time.time() - t0:.1f}s")
logger.info("AndesVL-1B 推理服务就绪!")
@asynccontextmanager
async def lifespan(app: FastAPI):
load_model()
yield
# ========== FastAPI 应用 ==========
app = FastAPI(title="VLM Server (AndesVL-1B)", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ========== Pydantic 模型 (OpenAI 格式) ==========
class ImageUrl(BaseModel):
url: str
class ContentPart(BaseModel):
type: str
text: str | None = None
image_url: ImageUrl | None = None
class Message(BaseModel):
role: str
content: str | list[ContentPart]
class ChatCompletionRequest(BaseModel):
model: str
messages: list[Message]
max_tokens: int = 100
temperature: float | None = None
# ========== 辅助函数 ==========
def decode_base64_image(data_url: str) -> Image.Image:
"""从 data URL 解码图像"""
if "," in data_url:
base64_str = data_url.split(",", 1)[1]
else:
base64_str = data_url
image_bytes = base64.b64decode(base64_str)
return Image.open(io.BytesIO(image_bytes)).convert("RGB")
def build_messages(messages: list[Message]) -> list[dict]:
"""OpenAI 格式 → AndesVL chat 格式"""
result = []
for msg in messages:
if isinstance(msg.content, str):
result.append({"role": msg.role, "content": msg.content})
else:
content_list = []
for part in msg.content:
if part.type == "text" and part.text:
content_list.append({"type": "text", "text": part.text})
elif part.type == "image_url" and part.image_url:
image = decode_base64_image(part.image_url.url)
content_list.append({"type": "image", "image": image})
result.append({"role": msg.role, "content": content_list})
return result
# ========== API 路由 ==========
@app.get("/v1/models")
async def list_models():
return {
"object": "list",
"data": [
{
"id": "./OpenGVLab/InternVL3-1B/",
"object": "model",
"created": 1700000000,
"owned_by": "local",
}
],
}
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
try:
messages = build_messages(request.messages)
logger.info(f"收到请求, model={request.model}, max_tokens={request.max_tokens}")
start_time = time.time()
response_text = model.chat(
messages,
tokenizer,
image_processor,
max_new_tokens=request.max_tokens,
)
elapsed = time.time() - start_time
logger.info(f"推理完成,耗时 {elapsed:.1f}s, 结果: {response_text[:50]}...")
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:12]}",
"object": "chat.completion",
"created": int(time.time()),
"model": request.model,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": response_text},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
}
except Exception as e:
logger.error(f"推理出错: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "ok", "model_loaded": model is not None}
# ========== 入口 ==========
if __name__ == "__main__":
logger.info("=" * 50)
logger.info("VLM 图生文推理服务启动中...")
logger.info(f"监听: http://{HOST}:{PORT}")
logger.info("=" * 50)
uvicorn.run(app, host=HOST, port=PORT, log_level="info")