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"
This commit is contained in:
2026-06-22 17:16:45 +08:00
parent a9bfeef59b
commit 779b32362a
51 changed files with 3974 additions and 398 deletions

207
vlm_server.py Normal file
View File

@@ -0,0 +1,207 @@
#!/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")