""" 万物有灵 (Animism) - 万物皆有灵,指哪说哪! 用 MiniCPM-o 4.5 赋予任何物体人格和声音 Gradio Web App for HuggingFace Space """ import gradio as gr from PIL import Image import base64 import io import os from personalities import PERSONALITY_TYPES, DEFAULT_PERSONALITY from model_utils import ( load_model, identify_object, chat_with_object, get_model_info, suggest_personality, ) # ==================== 配置 ==================== MODEL_PATH = os.environ.get("MODEL_PATH", "openbmb/MiniCPM-o-4_5") ENABLE_TTS = os.environ.get("ENABLE_TTS", "false").lower() == "true" # ==================== 应用状态 ==================== class AppState: """管理应用状态""" def __init__(self): self.reset() def reset(self): self.image = None self.object_info = {} self.personality_type = DEFAULT_PERSONALITY self.chat_history = [] self.is_identified = False def to_dict(self): d = { "object_info": self.object_info, "personality_type": self.personality_type, "chat_history": self.chat_history, "is_identified": self.is_identified, } if self.image is not None: buf = io.BytesIO() self.image.save(buf, format="PNG") d["image_b64"] = base64.b64encode(buf.getvalue()).decode("utf-8") return d def from_dict(self, d): if not d: self.reset() return self.object_info = d.get("object_info", {}) self.personality_type = d.get("personality_type", DEFAULT_PERSONALITY) self.chat_history = d.get("chat_history", []) self.is_identified = d.get("is_identified", False) if d.get("image_b64"): self.image = Image.open(io.BytesIO(base64.b64decode(d["image_b64"]))) else: self.image = None # ==================== 核心逻辑 ==================== _model_init_attempted = False def init_model(): """初始化模型(仅尝试一次,失败不阻塞启动)""" global _model_init_attempted _model_init_attempted = True try: load_model(model_path=MODEL_PATH, enable_tts=ENABLE_TTS) return "ready" except Exception as e: print(f"[Init] Failed to load model: {e}") print("[Init] Will retry when user interacts...") return f"error: {e}" def ensure_model_loaded(): """确保模型已加载,未加载则尝试加载""" from model_utils import is_model_loaded if is_model_loaded(): return True try: print("[Model] Retrying model load...") load_model(model_path=MODEL_PATH, enable_tts=ENABLE_TTS) return True except Exception as e: print(f"[Model] Retry failed: {e}") return False def _parse_personality(radio_value: str) -> str: """从 radio 选项值解析性格类型名称""" if not radio_value: return DEFAULT_PERSONALITY for p in PERSONALITY_TYPES: if radio_value.startswith(p): return p return DEFAULT_PERSONALITY def process_image(image, personality_radio, state_dict, chatbot): """处理上传图片:识别物体 + 生成自我介绍""" if image is None: return chatbot, state_dict, "请先上传或拍摄一张图片", get_model_info() # 确保模型已加载(支持启动失败后重试) if not ensure_model_loaded(): chatbot = chatbot + [ {"role": "assistant", "content": "❌ 模型加载失败,请检查日志或重启应用"} ] return chatbot, state_dict, "❌ 模型未就绪", get_model_info() state = AppState() state.from_dict(state_dict) # 转换图片 if isinstance(image, dict): image = image.get("composite", image.get("image")) if image is None: return chatbot, state_dict, "请先上传或拍摄一张图片", get_model_info() if not isinstance(image, Image.Image): image = Image.fromarray(image).convert("RGB") else: image = image.convert("RGB") state.image = image state.personality_type = _parse_personality(personality_radio) state.chat_history = [] state.is_identified = False # 识别物体 object_info = identify_object(image) if "error" in object_info: error_msg = object_info.get("error", "识别失败") chatbot = chatbot + [{"role": "assistant", "content": f"❌ 识别出错了:{error_msg}"}] return chatbot, state.to_dict(), f"❌ 识别失败", get_model_info() state.object_info = object_info state.is_identified = True object_name = object_info.get("object_name", "未知物体") # 自动建议性格 auto_personality = suggest_personality(object_name) if state.personality_type not in PERSONALITY_TYPES: state.personality_type = auto_personality persona = PERSONALITY_TYPES[state.personality_type] # 信息卡片 info_text = ( f"🔍 识别到:**{object_name}**\n\n" f"👀 外观:{object_info.get('appearance', '未知')}\n\n" f"📍 场景:{object_info.get('scene', '未知')}\n\n" f"🎭 性格:{state.personality_type} {persona['emoji']} {persona['description']}" ) # 让物体自我介绍 intro = chat_with_object( image=state.image, object_info=state.object_info, personality_type=state.personality_type, chat_history=[], user_message="你好!你是谁?", is_first_message=True, ) state.chat_history.append({"role": "user", "content": "你好!你是谁?"}) state.chat_history.append({"role": "assistant", "content": intro}) chatbot = chatbot + [ {"role": "assistant", "content": f"🎭 **{object_name}** 觉醒了!性格:{state.personality_type} {persona['emoji']}\n\n{intro}"} ] return chatbot, state.to_dict(), info_text, get_model_info() def send_message(user_message, state_dict, chatbot): """发送消息与物体对话""" if not user_message or not user_message.strip(): return chatbot, state_dict, "" if not ensure_model_loaded(): chatbot = chatbot + [ {"role": "user", "content": user_message}, {"role": "assistant", "content": "❌ 模型未就绪,请稍后再试"}, ] return chatbot, state_dict, "" state = AppState() state.from_dict(state_dict) if not state.is_identified or state.image is None: chatbot = chatbot + [ {"role": "user", "content": user_message}, {"role": "assistant", "content": "请先上传图片,让物体觉醒!"}, ] return chatbot, state.to_dict(), "" # 调用模型 response = chat_with_object( image=state.image, object_info=state.object_info, personality_type=state.personality_type, chat_history=state.chat_history, user_message=user_message, ) # 更新历史 state.chat_history.append({"role": "user", "content": user_message}) state.chat_history.append({"role": "assistant", "content": response}) chatbot = chatbot + [ {"role": "user", "content": user_message}, {"role": "assistant", "content": response}, ] return chatbot, state.to_dict(), "" def switch_personality(personality_radio, state_dict, chatbot): """切换性格类型""" state = AppState() state.from_dict(state_dict) if not state.is_identified or state.image is None: return chatbot, state.to_dict(), "请先上传图片唤醒物体" personality = _parse_personality(personality_radio) state.personality_type = personality state.chat_history = [] persona = PERSONALITY_TYPES[personality] object_name = state.object_info.get("object_name", "未知物体") # 新性格自我介绍 intro = chat_with_object( image=state.image, object_info=state.object_info, personality_type=personality, chat_history=[], user_message="你好!你是谁?", is_first_message=True, ) state.chat_history.append({"role": "user", "content": "你好!你是谁?"}) state.chat_history.append({"role": "assistant", "content": intro}) info_text = ( f"🔍 识别到:**{object_name}**\n\n" f"🎭 性格切换为:{personality} {persona['emoji']} {persona['description']}\n\n" f"💬 {intro}" ) chatbot = chatbot + [ {"role": "assistant", "content": f"🎭 性格切换为:{personality} {persona['emoji']}\n\n{intro}"} ] return chatbot, state.to_dict(), info_text def clear_chat(state_dict): """清除对话""" state = AppState() state.from_dict(state_dict) state.chat_history = [] return [], state.to_dict() def reset_all(): """完全重置""" state = AppState() return [], state.to_dict(), "等待上传图片...", None, get_model_info() # ==================== Gradio UI ==================== CSS = """ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); :root { --primary: #6C5CE7; --primary-light: #A29BFE; --accent: #FD79A8; } .gradio-container { font-family: 'Noto Sans SC', sans-serif !important; max-width: 1200px !important; } .app-title { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; font-size: 2.5rem; text-align: center; margin: 0; } .app-subtitle { text-align: center; color: #636e72; font-size: 1.1rem; margin-top: 0.5rem; } .object-info-card { background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%) !important; border: 1px solid #d1d9ff !important; border-radius: 16px !important; padding: 20px !important; box-shadow: 0 4px 12px rgba(108, 92, 231, 0.1) !important; } .quick-btn { font-size: 0.85rem !important; border-radius: 20px !important; margin: 2px !important; } """ def create_app(): """创建 Gradio 应用""" with gr.Blocks( title="万物有灵 - Animism", ) as app: state = gr.State(value=AppState().to_dict()) # ====== 标题 ====== gr.HTML( """
✨ 指哪说话,万物成精!对准任何物体,赋予它人格和声音 ✨
Powered by MiniCPM-o 4.5 全模态大模型
万物有灵 (Animism) | Powered by MiniCPM-o 4.5 | Hackathon Project
🔍 视觉识别 + 🎭 人格赋予 + 💬 对话交互