File size: 7,085 Bytes
69a9e5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
"""

万物有灵 - 模型加载与推理工具

使用 MiniCPM-o 4.5 实现物体识别和人格化对话

"""

import torch
from PIL import Image
from transformers import AutoModel, AutoTokenizer
from personalities import (
    build_identify_prompt,
    build_personality_system_prompt,
    build_followup_system_prompt,
    parse_identification,
    PERSONALITY_TYPES,
    OBJECT_PERSONALITY_HINTS,
    DEFAULT_PERSONALITY,
)
import re

# 全局模型引用
_model = None
_tokenizer = None
_device = None


def get_device():
    """获取可用设备"""
    if torch.cuda.is_available():
        return "cuda"
    return "cpu"


def load_model(model_path: str = "openbmb/MiniCPM-o-4_5", enable_tts: bool = False):
    """

    加载 MiniCPM-o 4.5 模型



    Args:

        model_path: 模型路径或 HuggingFace model ID

        enable_tts: 是否启用 TTS 语音合成(需要更多显存)

    """
    global _model, _tokenizer, _device

    _device = get_device()

    print(f"[Model] Loading model from {model_path} on {_device}...")

    _tokenizer = AutoTokenizer.from_pretrained(
        model_path, trust_remote_code=True
    )

    _model = AutoModel.from_pretrained(
        model_path,
        trust_remote_code=True,
        attn_implementation="sdpa",
        torch_dtype=torch.bfloat16,
        init_audio=enable_tts,
        init_tts=enable_tts,
    )

    _model = _model.to(device=_device, dtype=torch.bfloat16)
    _model.eval()

    if not enable_tts:
        print("[Model] Audio/TTS modules not loaded (save VRAM)")
        torch.cuda.empty_cache()

    print(f"[Model] Model loaded successfully on {_device}")
    return _model, _tokenizer


def is_model_loaded() -> bool:
    """检查模型是否已加载"""
    return _model is not None and _tokenizer is not None


def identify_object(image: Image.Image) -> dict:
    """

    识别图片中的物体



    Args:

        image: PIL Image 对象



    Returns:

        包含物体信息的字典

    """
    if not is_model_loaded():
        return {"error": "模型未加载,请稍候..."}

    prompt = build_identify_prompt()
    msgs = [{"role": "user", "content": [image, prompt]}]

    try:
        with torch.no_grad():
            response = _model.chat(
                image=None,
                msgs=msgs,
                tokenizer=_tokenizer,
                sampling=True,
                temperature=0.5,
                max_new_tokens=512,
            )

        result = parse_identification(response)
        result["raw_response"] = response
        return result

    except Exception as e:
        print(f"[Model] Error during identification: {e}")
        return {"error": str(e), "object_name": "未知物体"}


def chat_with_object(

    image: Image.Image,

    object_info: dict,

    personality_type: str,

    chat_history: list,

    user_message: str,

    is_first_message: bool = False,

) -> str:
    """

    与物体对话



    Args:

        image: 物体的图片

        object_info: 物体识别信息

        personality_type: 性格类型

        chat_history: 之前的对话历史 [{"role": "user/assistant", "content": "..."}]

        user_message: 用户当前消息



    Returns:

        模型的回复

    """
    if not is_model_loaded():
        return "模型未加载,请稍候..."

    object_name = object_info.get("object_name", "未知物体")
    appearance = object_info.get("appearance", "")
    scene = object_info.get("scene", "")
    suggestion = object_info.get("suggestion", "")

    # 构建 system prompt
    if is_first_message or len(chat_history) == 0:
        # 第一次对话:让物体自我介绍
        system_prompt = build_personality_system_prompt(
            object_name=object_name,
            object_appearance=appearance,
            object_scene=scene,
            personality_type=personality_type,
            personality_suggestion=suggestion,
        )
    else:
        # 后续对话:简洁版 system prompt
        id_info = f"外观:{appearance},场景:{scene}"
        system_prompt = build_followup_system_prompt(
            object_name=object_name,
            personality_type=personality_type,
            identification_info=id_info,
        )

    # 构建消息列表
    msgs = [{"role": "system", "content": system_prompt}]

    # 添加历史对话(第一条用户消息附带图片)
    for i, msg in enumerate(chat_history):
        if msg["role"] == "user":
            if i == 0:
                msgs.append({"role": "user", "content": [image, msg["content"]]})
            else:
                msgs.append({"role": "user", "content": msg["content"]})
        else:
            msgs.append({"role": "assistant", "content": msg["content"]})

    # 添加当前用户消息
    if is_first_message or len(chat_history) == 0:
        # 首次对话:附带图片的自我介绍请求
        msgs.append({"role": "user", "content": [image, "你好!你是谁?请用你的方式自我介绍一下。"]})
    else:
        msgs.append({"role": "user", "content": user_message})

    try:
        with torch.no_grad():
            response = _model.chat(
                image=None,
                msgs=msgs,
                tokenizer=_tokenizer,
                sampling=True,
                temperature=0.8,
                top_p=0.9,
                max_new_tokens=512,
            )

        # 清理响应中的特殊标记
        response = _clean_response(response)
        return response

    except Exception as e:
        print(f"[Model] Error during chat: {e}")
        return f"呜...我好像卡住了({e})"


def _clean_response(text: str) -> str:
    """清理模型响应中的特殊标记"""
    # 移除 <box> 标记
    text = re.sub(r"<box>.*?</box>", "", text)
    text = text.replace("<ref>", "").replace("</ref>", "")
    text = text.replace("<box>", "").replace("</box>", "")
    # 移除思考过程标记
    text = re.sub(r"<think.*?>.*?</think.*?>", "", text, flags=re.DOTALL)
    # 清理多余空白
    text = text.strip()
    return text


def suggest_personality(object_name: str) -> str:
    """

    根据物体名称建议性格类型



    Args:

        object_name: 物体名称



    Returns:

        建议的性格类型

    """
    for key, personality in OBJECT_PERSONALITY_HINTS.items():
        if key in object_name:
            return personality
    return DEFAULT_PERSONALITY


def get_model_info() -> str:
    """获取模型状态信息"""
    if not is_model_loaded():
        return "模型未加载"

    device_name = "CPU"
    if _device == "cuda":
        device_name = torch.cuda.get_device_name(0)
        vram = torch.cuda.get_device_properties(0).total_memory / 1024**3
        device_name = f"{device_name} ({vram:.1f}GB)"

    return f"MiniCPM-o 4.5 | 设备: {device_name}"