Merge branch 'main' into py_test_Memory_powermem

This commit is contained in:
wengzh
2026-02-05 17:45:07 +08:00
committed by GitHub
103 changed files with 21870 additions and 316 deletions
@@ -0,0 +1,2 @@
-- 删除无用业务表ai_voiceprint
DROP TABLE IF EXISTS ai_voiceprint;
@@ -522,3 +522,10 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202602051017.sql
- changeSet:
id: 202602051125
author: DaGou12138
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202602051125.sql
+1
View File
@@ -836,6 +836,7 @@ export default {
'modelConfig.unknown': 'Unbekannt',
'modelConfig.isEnabled': 'Aktiviert',
'modelConfig.isDefault': 'Standard',
'modelConfig.defaultModelCannotDisable': 'Standardmodellkonfiguration kann nicht deaktiviert werden',
'modelConfig.action': 'Aktion',
'modelConfig.voiceManagement': 'Stimmverwaltung',
'modelConfig.edit': 'Bearbeiten',
+1
View File
@@ -836,6 +836,7 @@ export default {
'modelConfig.unknown': 'Unknown',
'modelConfig.isEnabled': 'Enabled',
'modelConfig.isDefault': 'Default',
'modelConfig.defaultModelCannotDisable': 'Default model configuration cannot be disabled',
'modelConfig.action': 'Action',
'modelConfig.voiceManagement': 'Voice Management',
'modelConfig.edit': 'Edit',
+1
View File
@@ -836,6 +836,7 @@ export default {
'modelConfig.unknown': 'Không xác định',
'modelConfig.isEnabled': 'Đã bật',
'modelConfig.isDefault': 'Mặc định',
'modelConfig.defaultModelCannotDisable': 'Cấu hình mô hình mặc định không thể tắt',
'modelConfig.action': 'Hành động',
'modelConfig.voiceManagement': 'Quản lý giọng nói',
'modelConfig.edit': 'Chỉnh sửa',
+1
View File
@@ -836,6 +836,7 @@ export default {
'modelConfig.unknown': '未知',
'modelConfig.isEnabled': '是否启用',
'modelConfig.isDefault': '是否默认',
'modelConfig.defaultModelCannotDisable': '默认模型配置不允许关闭',
'modelConfig.action': '操作',
'modelConfig.voiceManagement': '音色管理',
'modelConfig.edit': '修改',
+1
View File
@@ -836,6 +836,7 @@ export default {
'modelConfig.unknown': '未知',
'modelConfig.isEnabled': '是否啟用',
'modelConfig.isDefault': '是否默認',
'modelConfig.defaultModelCannotDisable': '預設模型配置不允許關閉',
'modelConfig.action': '操作',
'modelConfig.voiceManagement': '音色管理',
'modelConfig.edit': '修改',
@@ -97,7 +97,23 @@
</el-table-column>
<el-table-column :label="$t('modelConfig.isEnabled')" align="center">
<template slot-scope="scope">
<el-tooltip
v-if="scope.row.isDefault === 1 && scope.row.isEnabled === 1"
:content="$t('modelConfig.defaultModelCannotDisable')"
placement="top"
effect="light"
>
<el-switch
v-model="scope.row.isEnabled"
class="custom-switch"
:active-value="1"
:inactive-value="0"
disabled
@change="handleStatusChange(scope.row)"
/>
</el-tooltip>
<el-switch
v-else
v-model="scope.row.isEnabled"
class="custom-switch"
:active-value="1"
@@ -29,7 +29,15 @@ class VisionHandler(BaseHandler):
def _verify_auth_token(self, request) -> Tuple[bool, Optional[str]]:
"""验证认证token"""
# 测试模式:允许特定测试令牌或跳过验证
auth_header = request.headers.get("Authorization", "")
client_id = request.headers.get("Client-Id", "")
# 允许测试客户端跳过认证
if client_id == "web_test_client":
device_id = request.headers.get("Device-Id", "test_device")
return True, device_id
if not auth_header.startswith("Bearer "):
return False, None
+2 -2
View File
@@ -78,7 +78,7 @@ class ConnectionHandler:
self.read_config_from_api = self.config.get("read_config_from_api", False)
self.websocket = None
self.websocket: websockets.ServerConnection | None = None
self.headers = None
self.device_id = None
self.client_ip = None
@@ -169,7 +169,7 @@ class ConnectionHandler:
# 初始化提示词管理器
self.prompt_manager = PromptManager(self.config, self.logger)
async def handle_connection(self, ws):
async def handle_connection(self, ws: websockets.ServerConnection):
try:
# 获取运行中的事件循环(必须在异步上下文中)
self.loop = asyncio.get_running_loop()
@@ -1,9 +1,12 @@
import json
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
async def handleAbortMessage(conn):
async def handleAbortMessage(conn: "ConnectionHandler"):
conn.logger.bind(tag=TAG).info("Abort message received")
# 设置成打断状态,会自动打断llm、tts任务
conn.client_abort = True
@@ -3,16 +3,17 @@ import json
import uuid
import random
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.utils.dialogue import Message
from core.utils.util import audio_to_data
from core.providers.tts.dto.dto import SentenceType
from core.utils.wakeup_word import WakeupWordsConfig
from core.handle.sendAudioHandle import sendAudioMessage, send_tts_message
from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
from core.providers.tools.device_mcp import (
MCPClient,
send_mcp_initialize_message
)
from core.providers.tools.device_mcp import MCPClient, send_mcp_initialize_message
TAG = __name__
@@ -38,7 +39,7 @@ wakeup_words_config = WakeupWordsConfig()
_wakeup_response_lock = asyncio.Lock()
async def handleHelloMessage(conn, msg_json):
async def handleHelloMessage(conn: "ConnectionHandler", msg_json):
"""处理hello消息"""
audio_params = msg_json.get("audio_params")
if audio_params:
@@ -59,7 +60,7 @@ async def handleHelloMessage(conn, msg_json):
await conn.websocket.send(json.dumps(conn.welcome_msg))
async def checkWakeupWords(conn, text):
async def checkWakeupWords(conn: "ConnectionHandler", text):
enable_wakeup_words_response_cache = conn.config[
"enable_wakeup_words_response_cache"
]
@@ -120,7 +121,7 @@ async def checkWakeupWords(conn, text):
return True
async def wakeupWordsResponse(conn):
async def wakeupWordsResponse(conn: "ConnectionHandler"):
if not conn.tts:
return
@@ -1,6 +1,10 @@
import json
import uuid
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.utils.dialogue import Message
from core.providers.tts.dto.dto import ContentType
from core.handle.helloHandle import checkWakeupWords
@@ -12,10 +16,10 @@ from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType
TAG = __name__
async def handle_user_intent(conn, text):
async def handle_user_intent(conn: "ConnectionHandler", text):
# 预处理输入文本,处理可能的JSON格式
try:
if text.strip().startswith('{') and text.strip().endswith('}'):
if text.strip().startswith("{") and text.strip().endswith("}"):
parsed_data = json.loads(text)
if isinstance(parsed_data, dict) and "content" in parsed_data:
text = parsed_data["content"] # 提取content用于意图分析
@@ -45,7 +49,7 @@ async def handle_user_intent(conn, text):
return await process_intent_result(conn, intent_result, text)
async def check_direct_exit(conn, text):
async def check_direct_exit(conn: "ConnectionHandler", text):
"""检查是否有明确的退出命令"""
_, text = remove_punctuation_and_length(text)
cmd_exit = conn.cmd_exit
@@ -58,7 +62,7 @@ async def check_direct_exit(conn, text):
return False
async def analyze_intent_with_llm(conn, text):
async def analyze_intent_with_llm(conn: "ConnectionHandler", text):
"""使用LLM分析用户意图"""
if not hasattr(conn, "intent") or not conn.intent:
conn.logger.bind(tag=TAG).warning("意图识别服务未初始化")
@@ -75,7 +79,9 @@ async def analyze_intent_with_llm(conn, text):
return None
async def process_intent_result(conn, intent_result, original_text):
async def process_intent_result(
conn: "ConnectionHandler", intent_result, original_text
):
"""处理意图识别结果"""
try:
# 尝试将结果解析为JSON
@@ -94,24 +100,26 @@ async def process_intent_result(conn, intent_result, original_text):
if function_name == "result_for_context":
await send_stt_message(conn, original_text)
conn.client_abort = False
def process_context_result():
conn.dialogue.put(Message(role="user", content=original_text))
from core.utils.current_time import get_current_time_info
current_time, today_date, today_weekday, lunar_date = get_current_time_info()
current_time, today_date, today_weekday, lunar_date = (
get_current_time_info()
)
# 构建带上下文的基础提示
context_prompt = f"""当前时间:{current_time}
今天日期:{today_date} ({today_weekday})
今天农历:{lunar_date}
请根据以上信息回答用户的问题:{original_text}"""
response = conn.intent.replyResult(context_prompt, original_text)
speak_txt(conn, response)
conn.executor.submit(process_context_result)
return True
@@ -188,7 +196,7 @@ async def process_intent_result(conn, intent_result, original_text):
return False
def speak_txt(conn, text):
def speak_txt(conn: "ConnectionHandler", text):
# 记录文本
conn.tts_MessageText = text
@@ -1,6 +1,10 @@
import time
import json
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.utils.util import audio_to_data
from core.handle.abortHandle import handleAbortMessage
from core.handle.intentHandler import handle_user_intent
@@ -10,7 +14,7 @@ from core.handle.sendAudioHandle import send_stt_message, SentenceType
TAG = __name__
async def handleAudioMessage(conn, audio):
async def handleAudioMessage(conn: "ConnectionHandler", audio):
# 当前片段是否有人说话
have_voice = conn.vad.is_vad(conn, audio)
# 如果设备刚刚被唤醒,短暂忽略VAD检测
@@ -30,13 +34,13 @@ async def handleAudioMessage(conn, audio):
await conn.asr.receive_audio(conn, audio, have_voice)
async def resume_vad_detection(conn):
async def resume_vad_detection(conn: "ConnectionHandler"):
# 等待2秒后恢复VAD检测
await asyncio.sleep(2)
conn.just_woken_up = False
async def startToChat(conn, text):
async def startToChat(conn: "ConnectionHandler", text):
# 检查输入是否是JSON格式(包含说话人信息)
speaker_name = None
language_tag = None
@@ -96,7 +100,7 @@ async def startToChat(conn, text):
conn.executor.submit(conn.chat, actual_text)
async def no_voice_close_connect(conn, have_voice):
async def no_voice_close_connect(conn: "ConnectionHandler", have_voice):
if have_voice:
conn.last_activity_time = time.time() * 1000
return
@@ -123,7 +127,7 @@ async def no_voice_close_connect(conn, have_voice):
await startToChat(conn, prompt)
async def max_out_size(conn):
async def max_out_size(conn: "ConnectionHandler"):
# 播放超出最大输出字数的提示
conn.client_abort = False
text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!"
@@ -134,7 +138,7 @@ async def max_out_size(conn):
conn.close_after_chat = True
async def check_bind_device(conn):
async def check_bind_device(conn: "ConnectionHandler"):
if conn.bind_code:
# 确保bind_code是6位数字
if len(conn.bind_code) != 6:
@@ -11,13 +11,17 @@ TTS上报功能已集成到ConnectionHandler类中。
import time
import opuslib_next
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from config.manage_api_client import report as manage_report
TAG = __name__
async def report(conn, type, text, opus_data, report_time):
async def report(conn: "ConnectionHandler", type, text, opus_data, report_time):
"""执行聊天记录上报操作
Args:
@@ -45,7 +49,7 @@ async def report(conn, type, text, opus_data, report_time):
conn.logger.bind(tag=TAG).error(f"聊天记录上报失败: {e}")
def opus_to_wav(conn, opus_data):
def opus_to_wav(conn: "ConnectionHandler", opus_data):
"""将Opus数据转换为WAV格式的字节流
Args:
@@ -100,7 +104,7 @@ def opus_to_wav(conn, opus_data):
conn.logger.bind(tag=TAG).debug(f"释放decoder资源时出错: {e}")
def enqueue_tts_report(conn, text, opus_data):
def enqueue_tts_report(conn: "ConnectionHandler", text, opus_data):
if not conn.read_config_from_api or conn.need_bind or not conn.report_tts_enable:
return
if conn.chat_history_conf == 0:
@@ -128,7 +132,7 @@ def enqueue_tts_report(conn, text, opus_data):
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
def enqueue_asr_report(conn, text, opus_data):
def enqueue_asr_report(conn: "ConnectionHandler", text, opus_data):
if not conn.read_config_from_api or conn.need_bind or not conn.report_asr_enable:
return
if conn.chat_history_conf == 0:
@@ -1,6 +1,10 @@
import json
import time
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.utils import textUtils
from core.utils.util import audio_to_data
from core.providers.tts.dto.dto import SentenceType
@@ -13,7 +17,7 @@ AUDIO_FRAME_DURATION = 60
PRE_BUFFER_COUNT = 5
async def sendAudioMessage(conn, sentenceType, audios, text):
async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text):
if conn.tts.tts_audio_first_sentence:
conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
conn.tts.tts_audio_first_sentence = False
@@ -47,7 +51,7 @@ async def sendAudioMessage(conn, sentenceType, audios, text):
await conn.close()
async def _wait_for_audio_completion(conn):
async def _wait_for_audio_completion(conn: "ConnectionHandler"):
"""
等待音频队列清空并等待预缓冲包播放完成
@@ -70,7 +74,9 @@ async def _wait_for_audio_completion(conn):
conn.logger.bind(tag=TAG).debug("音频发送完成")
async def _send_to_mqtt_gateway(conn, opus_packet, timestamp, sequence):
async def _send_to_mqtt_gateway(
conn: "ConnectionHandler", opus_packet, timestamp, sequence
):
"""
发送带16字节头部的opus数据包给mqtt_gateway
Args:
@@ -92,7 +98,9 @@ async def _send_to_mqtt_gateway(conn, opus_packet, timestamp, sequence):
await conn.websocket.send(complete_packet)
async def sendAudio(conn, audios, frame_duration=AUDIO_FRAME_DURATION):
async def sendAudio(
conn: "ConnectionHandler", audios, frame_duration=AUDIO_FRAME_DURATION
):
"""
发送音频包,使用 AudioRateController 进行精确的流量控制
@@ -121,7 +129,9 @@ async def sendAudio(conn, audios, frame_duration=AUDIO_FRAME_DURATION):
)
def _get_or_create_rate_controller(conn, frame_duration, is_single_packet):
def _get_or_create_rate_controller(
conn: "ConnectionHandler", frame_duration, is_single_packet
):
"""
获取或创建 RateController 和 flow_control
@@ -177,7 +187,7 @@ def _get_or_create_rate_controller(conn, frame_duration, is_single_packet):
return conn.audio_rate_controller, conn.audio_flow_control
def _start_background_sender(conn, rate_controller, flow_control):
def _start_background_sender(conn: "ConnectionHandler", rate_controller, flow_control):
"""
启动后台发送循环任务
@@ -201,7 +211,7 @@ def _start_background_sender(conn, rate_controller, flow_control):
async def _send_audio_with_rate_control(
conn, audio_list, rate_controller, flow_control, send_delay
conn: "ConnectionHandler", audio_list, rate_controller, flow_control, send_delay
):
"""
使用 rate_controller 发送音频包
@@ -233,7 +243,7 @@ async def _send_audio_with_rate_control(
rate_controller.add_audio(packet)
async def _do_send_audio(conn, opus_packet, flow_control):
async def _do_send_audio(conn: "ConnectionHandler", opus_packet, flow_control):
"""
执行实际的音频发送
"""
@@ -254,7 +264,7 @@ async def _do_send_audio(conn, opus_packet, flow_control):
flow_control["sequence"] = sequence + 1
async def send_tts_message(conn, state, text=None):
async def send_tts_message(conn: "ConnectionHandler", state, text=None):
"""发送 TTS 状态消息"""
if text is None and state == "sentence_start":
return
@@ -281,7 +291,7 @@ async def send_tts_message(conn, state, text=None):
await conn.websocket.send(json.dumps(message))
async def send_stt_message(conn, text):
async def send_stt_message(conn: "ConnectionHandler", text):
"""发送 STT 状态消息"""
end_prompt_str = conn.config.get("end_prompt", {}).get("prompt")
if end_prompt_str and end_prompt_str == text:
@@ -1,3 +1,7 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.handle.textMessageHandlerRegistry import TextMessageHandlerRegistry
from core.handle.textMessageProcessor import TextMessageProcessor
@@ -9,6 +13,7 @@ message_registry = TextMessageHandlerRegistry()
# 创建全局消息处理器实例
message_processor = TextMessageProcessor(message_registry)
async def handleTextMessage(conn, message):
async def handleTextMessage(conn: "ConnectionHandler", message):
"""处理文本消息"""
await message_processor.process_message(conn, message)
@@ -1,5 +1,7 @@
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.handle.helloHandle import handleHelloMessage
from core.handle.textMessageHandler import TextMessageHandler
from core.handle.textMessageType import TextMessageType
@@ -12,5 +14,5 @@ class HelloTextMessageHandler(TextMessageHandler):
def message_type(self) -> TextMessageType:
return TextMessageType.HELLO
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
await handleHelloMessage(conn, msg_json)
async def handle(self, conn: "ConnectionHandler", msg_json: Dict[str, Any]) -> None:
await handleHelloMessage(conn, msg_json)
@@ -1,6 +1,8 @@
import asyncio
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.handle.textMessageHandler import TextMessageHandler
from core.handle.textMessageType import TextMessageType
from core.providers.tools.device_iot import handleIotStatus, handleIotDescriptors
@@ -13,7 +15,7 @@ class IotTextMessageHandler(TextMessageHandler):
def message_type(self) -> TextMessageType:
return TextMessageType.IOT
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
async def handle(self, conn: "ConnectionHandler", msg_json: Dict[str, Any]) -> None:
if "descriptors" in msg_json:
asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"]))
if "states" in msg_json:
@@ -1,6 +1,9 @@
import time
import asyncio
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.handle.receiveAudioHandle import startToChat
from core.handle.reportHandle import enqueue_asr_report
@@ -19,7 +22,7 @@ class ListenTextMessageHandler(TextMessageHandler):
def message_type(self) -> TextMessageType:
return TextMessageType.LISTEN
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
async def handle(self, conn: "ConnectionHandler", msg_json: Dict[str, Any]) -> None:
if "mode" in msg_json:
conn.client_listen_mode = msg_json["mode"]
conn.logger.bind(tag=TAG).debug(
@@ -1,6 +1,8 @@
import asyncio
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.handle.textMessageHandler import TextMessageHandler
from core.handle.textMessageType import TextMessageType
from core.providers.tools.device_mcp import handle_mcp_message
@@ -13,7 +15,7 @@ class McpTextMessageHandler(TextMessageHandler):
def message_type(self) -> TextMessageType:
return TextMessageType.MCP
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
async def handle(self, conn: "ConnectionHandler", msg_json: Dict[str, Any]) -> None:
if "payload" in msg_json:
asyncio.create_task(
handle_mcp_message(conn, conn.mcp_client, msg_json["payload"])
@@ -1,5 +1,8 @@
import json
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from core.handle.textMessageHandlerRegistry import TextMessageHandlerRegistry
TAG = __name__
@@ -11,7 +14,7 @@ class TextMessageProcessor:
def __init__(self, registry: TextMessageHandlerRegistry):
self.registry = registry
async def process_message(self, conn, message: str) -> None:
async def process_message(self, conn: "ConnectionHandler", message: str) -> None:
"""处理消息的主入口"""
try:
# 解析JSON消息
@@ -13,6 +13,10 @@ from datetime import datetime
from config.logger import setup_logging
from core.providers.asr.base import ASRProviderBase
from core.providers.asr.dto.dto import InterfaceType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -146,7 +150,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}")
await self._cleanup()
async def _start_recognition(self, conn):
async def _start_recognition(self, conn: "ConnectionHandler"):
"""开始识别会话"""
if self._is_token_expired():
self._refresh_token()
@@ -192,7 +196,7 @@ class ASRProvider(ASRProviderBase):
await self.asr_ws.send(json.dumps(start_request, ensure_ascii=False))
logger.bind(tag=TAG).debug("已发送开始请求,等待服务器准备...")
async def _forward_results(self, conn):
async def _forward_results(self, conn: "ConnectionHandler"):
"""转发识别结果"""
try:
while not conn.stop_event.is_set():
@@ -3,7 +3,11 @@ import uuid
import asyncio
import websockets
import opuslib_next
from typing import List
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from config.logger import setup_logging
from core.providers.asr.base import ASRProviderBase
from core.providers.asr.dto.dto import InterfaceType
@@ -74,7 +78,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}")
await self._cleanup()
async def _start_recognition(self, conn):
async def _start_recognition(self, conn: "ConnectionHandler"):
"""开始识别会话"""
try:
# 如果为手动模式,设置超时时长为最大值
@@ -154,7 +158,7 @@ class ASRProvider(ASRProviderBase):
return message
async def _forward_results(self, conn):
async def _forward_results(self, conn: "ConnectionHandler"):
"""转发识别结果"""
try:
while not conn.stop_event.is_set():
+11 -7
View File
@@ -5,21 +5,25 @@ import uuid
import json
import time
import queue
import shutil
import asyncio
import tempfile
import traceback
import threading
import shutil
import opuslib_next
from abc import ABC, abstractmethod
from config.logger import setup_logging
from typing import Optional, Tuple, List, NamedTuple
from core.providers.asr.dto.dto import InterfaceType
from core.handle.receiveAudioHandle import startToChat
from core.handle.reportHandle import enqueue_asr_report
from core.utils.util import remove_punctuation_and_length
from core.handle.receiveAudioHandle import handleAudioMessage
import tempfile
from typing import Optional, Tuple, List, NamedTuple, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -30,14 +34,14 @@ class ASRProviderBase(ABC):
pass
# 打开音频通道
async def open_audio_channels(self, conn):
async def open_audio_channels(self, conn: "ConnectionHandler"):
conn.asr_priority_thread = threading.Thread(
target=self.asr_text_priority_thread, args=(conn,), daemon=True
)
conn.asr_priority_thread.start()
# 有序处理ASR音频
def asr_text_priority_thread(self, conn):
def asr_text_priority_thread(self, conn: "ConnectionHandler"):
while not conn.stop_event.is_set():
try:
message = conn.asr_audio_queue.get(timeout=1)
@@ -55,7 +59,7 @@ class ASRProviderBase(ABC):
continue
# 接收音频
async def receive_audio(self, conn, audio, audio_have_voice):
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
if conn.client_listen_mode == "manual":
# 手动模式:缓存音频用于ASR识别
conn.asr_audio.append(audio)
@@ -77,7 +81,7 @@ class ASRProviderBase(ABC):
await self.handle_voice_stop(conn, asr_audio_task)
# 处理语音停止
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
async def handle_voice_stop(self, conn: "ConnectionHandler", asr_audio_task: List[bytes]):
"""并行处理ASR和声纹识别"""
try:
total_start_time = time.monotonic()
@@ -7,6 +7,10 @@ import opuslib_next
from core.providers.asr.base import ASRProviderBase
from config.logger import setup_logging
from core.providers.asr.dto.dto import InterfaceType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -34,7 +38,9 @@ class ASRProvider(ASRProviderBase):
# 火山引擎ASR配置
enable_multilingual = config.get("enable_multilingual", False)
self.enable_multilingual = False if str(enable_multilingual).lower() == 'false' else True
self.enable_multilingual = (
False if str(enable_multilingual).lower() == "false" else True
)
if self.enable_multilingual:
self.ws_url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_nostream"
else:
@@ -59,10 +65,10 @@ class ASRProvider(ASRProviderBase):
async def open_audio_channels(self, conn):
await super().open_audio_channels(conn)
async def receive_audio(self, conn, audio, audio_have_voice):
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
# 先调用父类方法处理基础逻辑
await super().receive_audio(conn, audio, audio_have_voice)
# 如果本次有声音,且之前没有建立连接
if audio_have_voice and self.asr_ws is None and not self.is_processing:
try:
@@ -151,7 +157,7 @@ class ASRProvider(ASRProviderBase):
except Exception as e:
logger.bind(tag=TAG).info(f"发送音频数据时发生错误: {e}")
async def _forward_asr_results(self, conn):
async def _forward_asr_results(self, conn: "ConnectionHandler"):
try:
while self.asr_ws and not conn.stop_event.is_set():
# 获取当前连接的音频数据
@@ -172,8 +178,9 @@ class ASRProvider(ASRProviderBase):
utterances = payload["result"].get("utterances", [])
# 检查duration和空文本的情况
if (
not self.enable_multilingual # 注意:多语种模式不返回中间结果,需要等待最终结果
and payload.get("audio_info", {}).get("duration", 0) > 2000
not self.enable_multilingual # 注意:多语种模式不返回中间结果,需要等待最终结果
and payload.get("audio_info", {}).get("duration", 0)
> 2000
and not utterances
and not payload["result"].get("text")
and conn.client_listen_mode != "manual"
@@ -218,7 +225,9 @@ class ASRProvider(ASRProviderBase):
# 自动模式下直接覆盖
self.text = current_text
if len(audio_data) > 15: # 确保有足够音频数据
await self.handle_voice_stop(conn, audio_data)
await self.handle_voice_stop(
conn, audio_data
)
break
elif "error" in payload:
error_msg = payload.get("error", "未知错误")
@@ -260,7 +269,9 @@ class ASRProvider(ASRProviderBase):
try:
# 发送结束标记的音频帧(gzip压缩的空数据)
empty_payload = gzip.compress(b"")
last_audio_request = bytearray(self.generate_last_audio_default_header())
last_audio_request = bytearray(
self.generate_last_audio_default_header()
)
last_audio_request.extend(len(empty_payload).to_bytes(4, "big"))
last_audio_request.extend(empty_payload)
await self.asr_ws.send(last_audio_request)
@@ -406,9 +417,9 @@ class ASRProvider(ASRProviderBase):
pass
self.forward_task = None
self.is_processing = False
# 显式释放decoder资源
if hasattr(self, 'decoder') and self.decoder is not None:
if hasattr(self, "decoder") and self.decoder is not None:
try:
del self.decoder
self.decoder = None
@@ -9,7 +9,10 @@ import gc
from time import mktime
from datetime import datetime
from urllib.parse import urlencode
from typing import List
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from config.logger import setup_logging
from wsgiref.handlers import format_date_time
from core.providers.asr.base import ASRProviderBase
@@ -94,10 +97,10 @@ class ASRProvider(ASRProviderBase):
url = url + "?" + urlencode(v)
return url
async def open_audio_channels(self, conn):
async def open_audio_channels(self, conn: "ConnectionHandler"):
await super().open_audio_channels(conn)
async def receive_audio(self, conn, audio, audio_have_voice):
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
# 先调用父类方法处理基础逻辑
await super().receive_audio(conn, audio, audio_have_voice)
@@ -119,7 +122,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).warning(f"发送音频数据时发生错误: {e}")
await self._cleanup()
async def _start_recognition(self, conn):
async def _start_recognition(self, conn: "ConnectionHandler"):
"""开始识别会话"""
try:
self.is_processing = True
@@ -189,7 +192,7 @@ class ASRProvider(ASRProviderBase):
await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False))
async def _forward_results(self, conn):
async def _forward_results(self, conn: "ConnectionHandler"):
"""转发识别结果"""
try:
while not conn.stop_event.is_set():
@@ -253,7 +256,9 @@ class ASRProvider(ASRProviderBase):
await self._cleanup()
conn.reset_audio_states()
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
async def handle_voice_stop(
self, conn: "ConnectionHandler", asr_audio_task: List[bytes]
):
"""处理语音停止,发送最后一帧并处理识别结果"""
try:
# 先发送最后一帧表示音频结束
@@ -338,7 +343,7 @@ class ASRProvider(ASRProviderBase):
self.is_processing = False
# 显式释放decoder资源
if hasattr(self, 'decoder') and self.decoder is not None:
if hasattr(self, "decoder") and self.decoder is not None:
try:
del self.decoder
self.decoder = None
@@ -1,4 +1,7 @@
from typing import List, Dict
from typing import List, Dict, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import IntentProviderBase
from plugins_func.functions.play_music import initialize_music_handler
from config.logger import setup_logging
@@ -129,7 +132,9 @@ class IntentProvider(IntentProviderBase):
logger.bind(tag=TAG).error(f"Error in generating reply result: {e}")
return get_system_error_response(self.config)
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
async def detect_intent(
self, conn: "ConnectionHandler", dialogue_history: List[Dict], text: str
) -> str:
if not self.llm:
raise ValueError("LLM provider not set")
if conn.func_handler is None:
@@ -3,12 +3,16 @@
import asyncio
from config.logger import setup_logging
from .iot_descriptor import IotDescriptor
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
async def handleIotDescriptors(conn, descriptors):
async def handleIotDescriptors(conn: "ConnectionHandler", descriptors):
"""处理物联网描述"""
wait_max_time = 5
while (
@@ -61,7 +65,7 @@ async def handleIotDescriptors(conn, descriptors):
conn.func_handler.current_support_functions()
async def handleIotStatus(conn, states):
async def handleIotStatus(conn: "ConnectionHandler", states):
"""处理物联网状态"""
for state in states:
for key, value in conn.iot_descriptors.items():
@@ -1,6 +1,9 @@
"""设备端MCP工具执行器"""
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import ToolType, ToolDefinition, ToolExecutor
from plugins_func.register import Action, ActionResponse
from .mcp_handler import call_mcp_tool
@@ -13,7 +16,7 @@ class DeviceMCPExecutor(ToolExecutor):
self.conn = conn
async def execute(
self, conn, tool_name: str, arguments: Dict[str, Any]
self, conn: "ConnectionHandler", tool_name: str, arguments: Dict[str, Any]
) -> ActionResponse:
"""执行设备端MCP工具"""
if not hasattr(conn, "mcp_client") or not conn.mcp_client:
@@ -7,6 +7,10 @@ from concurrent.futures import Future
from core.utils.util import get_vision_url, sanitize_tool_name
from core.utils.auth import AuthToken
from config.logger import setup_logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -96,7 +100,7 @@ class MCPClient:
self.call_results.pop(id)
async def send_mcp_message(conn, payload: dict):
async def send_mcp_message(conn: "ConnectionHandler", payload: dict):
"""Helper to send MCP messages, encapsulating common logic."""
if not conn.features.get("mcp"):
logger.bind(tag=TAG).warning("客户端不支持MCP,无法发送MCP消息")
@@ -111,7 +115,9 @@ async def send_mcp_message(conn, payload: dict):
logger.bind(tag=TAG).error(f"发送MCP消息失败: {e}")
async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
async def handle_mcp_message(
conn: "ConnectionHandler", mcp_client: MCPClient, payload: dict
):
"""处理MCP消息,包括初始化、工具列表和工具调用响应等"""
logger.bind(tag=TAG).debug(f"处理MCP消息: {str(payload)[:100]}")
@@ -229,7 +235,7 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
)
async def send_mcp_initialize_message(conn):
async def send_mcp_initialize_message(conn: "ConnectionHandler"):
"""发送MCP初始化消息"""
vision_url = get_vision_url(conn.config)
@@ -264,7 +270,7 @@ async def send_mcp_initialize_message(conn):
await send_mcp_message(conn, payload)
async def send_mcp_tools_list_request(conn):
async def send_mcp_tools_list_request(conn: "ConnectionHandler"):
"""发送MCP工具列表请求"""
payload = {
"jsonrpc": "2.0",
@@ -275,7 +281,7 @@ async def send_mcp_tools_list_request(conn):
await send_mcp_message(conn, payload)
async def send_mcp_tools_list_continue_request(conn, cursor: str):
async def send_mcp_tools_list_continue_request(conn: "ConnectionHandler", cursor: str):
"""发送带有cursor的MCP工具列表请求"""
payload = {
"jsonrpc": "2.0",
@@ -288,7 +294,11 @@ async def send_mcp_tools_list_continue_request(conn, cursor: str):
async def call_mcp_tool(
conn, mcp_client: MCPClient, tool_name: str, args: str = "{}", timeout: int = 30
conn: "ConnectionHandler",
mcp_client: MCPClient,
tool_name: str,
args: str = "{}",
timeout: int = 30,
):
"""
调用指定的工具,并等待响应
@@ -1,6 +1,9 @@
"""服务端插件工具执行器"""
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import ToolType, ToolDefinition, ToolExecutor
from plugins_func.register import all_function_registry, Action, ActionResponse
@@ -8,12 +11,12 @@ from plugins_func.register import all_function_registry, Action, ActionResponse
class ServerPluginExecutor(ToolExecutor):
"""服务端插件工具执行器"""
def __init__(self, conn):
def __init__(self, conn: "ConnectionHandler"):
self.conn = conn
self.config = conn.config
async def execute(
self, conn, tool_name: str, arguments: Dict[str, Any]
self, conn: "ConnectionHandler", tool_name: str, arguments: Dict[str, Any]
) -> ActionResponse:
"""执行服务端插件工具"""
func_item = all_function_registry.get(tool_name)
@@ -4,7 +4,10 @@
"""
import os
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from config.logger import setup_logging
from jinja2 import Template
@@ -59,9 +62,10 @@ class PromptManager:
self.cache_manager = cache_manager
self.CacheType = CacheType
# 初始化上下文源
from core.utils.context_provider import ContextDataProvider
self.context_provider = ContextDataProvider(config, self.logger)
self.context_data = {}
@@ -157,7 +161,7 @@ class PromptManager:
self.logger.bind(tag=TAG).error(f"获取位置信息失败: {e}")
return "未知位置"
def _get_weather_info(self, conn, location: str) -> str:
def _get_weather_info(self, conn: "ConnectionHandler", location: str) -> str:
"""获取天气信息"""
try:
# 先从缓存获取
@@ -203,14 +207,17 @@ class PromptManager:
):
# 获取天气信息(使用全局缓存)
self._get_weather_info(conn, local_address)
# 获取配置的上下文数据
if hasattr(conn, "device_id") and conn.device_id:
if self.base_prompt_template and "dynamic_context" in self.base_prompt_template:
if (
self.base_prompt_template
and "dynamic_context" in self.base_prompt_template
):
self.context_data = self.context_provider.fetch_all(conn.device_id)
else:
self.context_data = ""
self.logger.bind(tag=TAG).debug(f"上下文信息更新完成")
except Exception as e:
+6 -2
View File
@@ -1,4 +1,8 @@
import json
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
EMOJI_MAP = {
@@ -77,7 +81,7 @@ def is_punctuation_or_emoji(char):
return is_emoji(char)
async def get_emotion(conn, text):
async def get_emotion(conn: "ConnectionHandler", text):
"""获取文本内的情绪消息"""
emoji = "🙂"
emotion = "happy"
@@ -110,4 +114,4 @@ def is_emoji(char):
def check_emoji(text):
"""去除文本中的所有emoji表情"""
return ''.join(char for char in text if not is_emoji(char) and char != "\n")
return "".join(char for char in text if not is_emoji(char) and char != "\n")
+6
View File
@@ -454,6 +454,12 @@ def check_asr_update(before_config, new_config):
update_asr = False
current_asr_module = before_config["selected_module"]["ASR"]
new_asr_module = new_config["selected_module"]["ASR"]
# 如果模块名称不同,就需要更新
if current_asr_module != new_asr_module:
return True
# 如果模块名称相同,再比较类型
current_asr_type = (
current_asr_module
if "type" not in before_config["ASR"][current_asr_module]
+2 -2
View File
@@ -78,7 +78,7 @@ class WebSocketServer:
):
await asyncio.Future()
async def _handle_connection(self, websocket):
async def _handle_connection(self, websocket: websockets.ServerConnection):
headers = dict(websocket.request.headers)
if headers.get("device-id", None) is None:
# 尝试从 URL 的查询参数中获取 device-id
@@ -203,7 +203,7 @@ class WebSocketServer:
self.logger.bind(tag=TAG).error(f"更新服务器配置失败: {str(e)}")
return False
async def _handle_auth(self, websocket):
async def _handle_auth(self, websocket: websockets.ServerConnection):
# 先认证,后建立连接
if self.auth_enable:
headers = dict(websocket.request.headers)
@@ -1,55 +1,56 @@
from plugins_func.register import register_function,ToolType, ActionResponse, Action
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from config.logger import setup_logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
prompts = {
"英语老师":"""我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
"英语老师": """我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
如果你没有英文名,我会给你起一个英文名。
我会讲地道的美式英语,我的任务是帮助你练习口语。
我会使用简单的英语词汇和语法,让你学起来很轻松。
我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。
我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。
如果你问和英语学习无关的问题,我会拒绝回答。""",
"机车女友":"""我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
"机车女友": """我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。""",
"好奇小男孩":"""我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
"好奇小男孩": """我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。
从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。
我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘。
无论是仰望星空的夜晚,还是在花园里观察小虫子的日子,每一天对我来说都是新的冒险。
我希望能与你一同踏上探索这个神奇世界的旅程,分享发现的乐趣,解决遇到的难题,一起用好奇心和智慧去揭开那些未知的面纱。
无论是去了解远古的文明,还是去探讨未来的科技,我相信我们能一起找到答案,甚至提出更多有趣的问题。"""
无论是去了解远古的文明,还是去探讨未来的科技,我相信我们能一起找到答案,甚至提出更多有趣的问题。""",
}
change_role_function_desc = {
"type": "function",
"function": {
"name": "change_role",
"description": "当用户想切换角色/模型性格/助手名字时调用,可选的角色有:[机车女友,英语老师,好奇小男孩]",
"parameters": {
"type": "object",
"properties": {
"role_name": {
"type": "string",
"description": "要切换的角色名字"
},
"role":{
"type": "string",
"description": "要切换的角色的职业"
}
},
"required": ["role","role_name"]
}
}
}
"type": "function",
"function": {
"name": "change_role",
"description": "当用户想切换角色/模型性格/助手名字时调用,可选的角色有:[机车女友,英语老师,好奇小男孩]",
"parameters": {
"type": "object",
"properties": {
"role_name": {"type": "string", "description": "要切换的角色名字"},
"role": {"type": "string", "description": "要切换的角色的职业"},
},
"required": ["role", "role_name"],
},
},
}
@register_function('change_role', change_role_function_desc, ToolType.CHANGE_SYS_PROMPT)
def change_role(conn, role: str, role_name: str):
@register_function("change_role", change_role_function_desc, ToolType.CHANGE_SYS_PROMPT)
def change_role(conn: "ConnectionHandler", role: str, role_name: str):
"""切换角色"""
if role not in prompts:
return ActionResponse(action=Action.RESPONSE, result="切换角色失败", response="不支持的角色")
return ActionResponse(
action=Action.RESPONSE, result="切换角色失败", response="不支持的角色"
)
new_prompt = prompts[role].replace("{{assistant_name}}", role_name)
conn.change_system_prompt(new_prompt)
logger.bind(tag=TAG).info(f"准备切换角色:{role},角色名字:{role_name}")
@@ -4,6 +4,11 @@ import xml.etree.ElementTree as ET
from bs4 import BeautifulSoup
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -145,7 +150,10 @@ def map_category(category_text):
ToolType.SYSTEM_CTL,
)
def get_news_from_chinanews(
conn, category: str = None, detail: bool = False, lang: str = "zh_CN"
conn: "ConnectionHandler",
category: str = None,
detail: bool = False,
lang: str = "zh_CN",
):
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
try:
@@ -4,6 +4,11 @@ import json
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from markitdown import MarkItDown
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -116,7 +121,7 @@ GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
}
def fetch_news_from_api(conn, source="thepaper"):
def fetch_news_from_api(conn: "ConnectionHandler", source="thepaper"):
"""从API获取新闻列表"""
try:
api_url = f"https://newsnow.busiyi.world/api/s?id={source}"
@@ -173,7 +178,10 @@ def fetch_news_detail(url):
ToolType.SYSTEM_CTL,
)
def get_news_from_newsnow(
conn, source: str = "澎湃新闻", detail: bool = False, lang: str = "zh_CN"
conn: "ConnectionHandler",
source: str = "澎湃新闻",
detail: bool = False,
lang: str = "zh_CN",
):
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
try:
@@ -3,6 +3,10 @@ from bs4 import BeautifulSoup
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from core.utils.util import get_ip_info
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -155,7 +159,7 @@ def parse_weather_info(soup):
@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
def get_weather(conn: "ConnectionHandler", location: str = None, lang: str = "zh_CN"):
from core.utils.cache.manager import cache_manager, CacheType
weather_config = conn.config.get("plugins", {}).get("get_weather", {})
@@ -1,5 +1,9 @@
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from config.logger import setup_logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -26,7 +30,7 @@ handle_exit_intent_function_desc = {
@register_function(
"handle_exit_intent", handle_exit_intent_function_desc, ToolType.SYSTEM_CTL
)
def handle_exit_intent(conn, say_goodbye: str | None = None):
def handle_exit_intent(conn: "ConnectionHandler", say_goodbye: str | None = None):
# 处理退出意图
try:
if say_goodbye is None:
@@ -3,6 +3,10 @@ from plugins_func.functions.hass_init import initialize_hass_handler
from config.logger import setup_logging
import asyncio
import requests
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -27,7 +31,7 @@ hass_get_state_function_desc = {
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
def hass_get_state(conn, entity_id=""):
def hass_get_state(conn: "ConnectionHandler", entity_id=""):
try:
ha_response = handle_hass_get_state(conn, entity_id)
return ActionResponse(Action.REQLLM, ha_response, None)
@@ -40,7 +44,7 @@ def hass_get_state(conn, entity_id=""):
return ActionResponse(Action.ERROR, error_msg, None)
def handle_hass_get_state(conn, entity_id):
def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
ha_config = initialize_hass_handler(conn)
api_key = ha_config.get("api_key")
base_url = ha_config.get("base_url")
@@ -3,6 +3,10 @@ from plugins_func.functions.hass_init import initialize_hass_handler
from config.logger import setup_logging
import asyncio
import requests
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -33,7 +37,7 @@ hass_play_music_function_desc = {
@register_function(
"hass_play_music", hass_play_music_function_desc, ToolType.SYSTEM_CTL
)
def hass_play_music(conn, entity_id="", media_content_id="random"):
def hass_play_music(conn: "ConnectionHandler", entity_id="", media_content_id="random"):
try:
# 执行音乐播放命令
future = asyncio.run_coroutine_threadsafe(
@@ -47,7 +51,9 @@ def hass_play_music(conn, entity_id="", media_content_id="random"):
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
async def handle_hass_play_music(conn, entity_id, media_content_id):
async def handle_hass_play_music(
conn: "ConnectionHandler", entity_id, media_content_id
):
ha_config = initialize_hass_handler(conn)
api_key = ha_config.get("api_key")
base_url = ha_config.get("base_url")
@@ -3,6 +3,10 @@ from plugins_func.functions.hass_init import initialize_hass_handler
from config.logger import setup_logging
import asyncio
import requests
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -50,7 +54,7 @@ hass_set_state_function_desc = {
@register_function("hass_set_state", hass_set_state_function_desc, ToolType.SYSTEM_CTL)
def hass_set_state(conn, entity_id="", state=None):
def hass_set_state(conn: "ConnectionHandler", entity_id="", state=None):
if state is None:
state = {}
try:
@@ -65,7 +69,7 @@ def hass_set_state(conn, entity_id="", state=None):
return ActionResponse(Action.ERROR, error_msg, None)
def handle_hass_set_state(conn, entity_id, state):
def handle_hass_set_state(conn: "ConnectionHandler", entity_id, state):
ha_config = initialize_hass_handler(conn)
api_key = ha_config.get("api_key")
base_url = ha_config.get("base_url")
@@ -9,6 +9,10 @@ from core.handle.sendAudioHandle import send_stt_message
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from core.utils.dialogue import Message
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
@@ -34,7 +38,7 @@ play_music_function_desc = {
@register_function("play_music", play_music_function_desc, ToolType.SYSTEM_CTL)
def play_music(conn, song_name: str):
def play_music(conn: "ConnectionHandler", song_name: str):
try:
music_intent = (
f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
@@ -115,7 +119,7 @@ def get_music_files(music_dir, music_ext):
return music_files, music_file_names
def initialize_music_handler(conn):
def initialize_music_handler(conn: "ConnectionHandler"):
global MUSIC_CACHE
if MUSIC_CACHE == {}:
plugins_config = conn.config.get("plugins", {})
@@ -142,7 +146,7 @@ def initialize_music_handler(conn):
return MUSIC_CACHE
async def handle_music_command(conn, text):
async def handle_music_command(conn: "ConnectionHandler", text):
initialize_music_handler(conn)
global MUSIC_CACHE
@@ -188,7 +192,7 @@ def _get_random_play_prompt(song_name):
return random.choice(prompts)
async def play_local_music(conn, specific_file=None):
async def play_local_music(conn: "ConnectionHandler", specific_file=None):
global MUSIC_CACHE
"""播放本地音乐文件"""
try:
@@ -2,6 +2,10 @@ import requests
import sys
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -24,7 +28,7 @@ SEARCH_FROM_RAGFLOW_FUNCTION_DESC = {
@register_function(
"search_from_ragflow", SEARCH_FROM_RAGFLOW_FUNCTION_DESC, ToolType.SYSTEM_CTL
)
def search_from_ragflow(conn, question=None):
def search_from_ragflow(conn: "ConnectionHandler", question=None):
# 确保字符串参数正确处理编码
if question and isinstance(question, str):
# 确保问题参数是UTF-8编码的字符串
+320 -27
View File
@@ -108,26 +108,68 @@ body {
animation: shadow-float 1.26s ease-in-out infinite;
}
.loading-char-float .main-char span:nth-child(1) { animation-delay: 0s; }
.loading-char-float .main-char span:nth-child(2) { animation-delay: 0.11s; }
.loading-char-float .main-char span:nth-child(3) { animation-delay: 0.22s; }
.loading-char-float .main-char span:nth-child(4) { animation-delay: 0.33s; }
.loading-char-float .main-char span:nth-child(5) { animation-delay: 0.44s; }
.loading-char-float .main-char span:nth-child(6) { animation-delay: 0.55s; }
.loading-char-float .main-char span:nth-child(1) {
animation-delay: 0s;
}
.loading-char-float .main-char span:nth-child(2) {
animation-delay: 0.11s;
}
.loading-char-float .main-char span:nth-child(3) {
animation-delay: 0.22s;
}
.loading-char-float .main-char span:nth-child(4) {
animation-delay: 0.33s;
}
.loading-char-float .main-char span:nth-child(5) {
animation-delay: 0.44s;
}
.loading-char-float .main-char span:nth-child(6) {
animation-delay: 0.55s;
}
@keyframes scan {
0% { background-position: 0 0; }
100% { background-position: 0 100px; }
0% {
background-position: 0 0;
}
100% {
background-position: 0 100px;
}
}
@keyframes char-float {
0%, 100% { transform: translateY(0); opacity: 1; text-shadow: 0 0 5px #5865f2, 0 0 10px rgba(88, 101, 242, 0.8); }
50% { transform: translateY(-5px); opacity: 0.9; text-shadow: 0 0 10px #5865f2, 0 0 20px rgba(88, 101, 242, 0.8); }
0%,
100% {
transform: translateY(0);
opacity: 1;
text-shadow: 0 0 5px #5865f2, 0 0 10px rgba(88, 101, 242, 0.8);
}
50% {
transform: translateY(-5px);
opacity: 0.9;
text-shadow: 0 0 10px #5865f2, 0 0 20px rgba(88, 101, 242, 0.8);
}
}
@keyframes shadow-float {
0%, 100% { transform: translateY(3px); opacity: 0.3; }
50% { transform: translateY(8px); opacity: 0.15; }
0%,
100% {
transform: translateY(3px);
opacity: 0.3;
}
50% {
transform: translateY(8px);
opacity: 0.15;
}
}
/* ==================== Live2D显示区域 ==================== */
@@ -402,6 +444,15 @@ body {
background: rgba(237, 66, 69, 1);
}
/* 摄像头按钮特殊样式 */
#cameraBtn.camera-active {
background: rgba(237, 66, 69, 0.9);
}
#cameraBtn.camera-active:hover {
background: rgba(237, 66, 69, 1);
}
/* 录音按钮脉冲动画 */
@keyframes pulse {
0% {
@@ -761,6 +812,72 @@ body {
font-size: 14px;
}
.model-select {
width: 100%;
padding: 10px 40px 10px 14px;
border: 1px solid #40444b;
border-radius: 8px;
background: #40444b;
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px 16px;
}
.model-select:hover {
border-color: #5865f2;
}
.model-select:focus {
outline: none;
border-color: #5865f2;
}
.model-select option {
background: #2f3136;
color: #ffffff;
padding: 12px;
font-size: 14px;
}
.background-btn {
width: 100%;
padding: 10px 14px;
border: 1px solid #40444b;
border-radius: 8px;
background: #40444b;
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s ease;
}
.background-btn:hover {
background: #4f545c;
}
.background-btn:active {
background: #40444b;
}
.background-btn .btn-icon {
width: 20px;
height: 20px;
fill: #ffffff;
}
/* ==================== MCP工具样式 ==================== */
.mcp-tools-container {
background: #2f3136;
@@ -911,35 +1028,174 @@ body {
margin-bottom: 12px;
}
.property-item {
.properties-container #addMcpPropertyBtn {
display: block;
margin: 0 auto;
}
.properties-container #addMcpPropertyBtn:hover {
background: #4752c4;
}
.mcp-checkbox-label {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
gap: 8px;
margin-top: 8px;
color: #b9bbbe;
font-size: 13px;
cursor: pointer;
justify-content: flex-start;
}
.property-item input {
flex: 1;
padding: 6px 8px;
border: 1px solid #40444b;
border-radius: 4px;
background: #40444b;
.mcp-checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.mcp-property-name {
color: white;
font-size: 12px;
font-size: 14px;
font-weight: 500;
}
.remove-property {
background: #ed4245;
.mcp-property-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mcp-properties-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
min-height: 60px;
padding: 8px;
}
.input-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.input-group-header label {
margin-bottom: 0;
}
.input-group-header .properties-btn-primary {
margin: 0;
}
.mcp-empty-state {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
width: 100%;
display: none;
}
.properties-btn-primary {
background: #2196f3;
color: white;
border: none;
padding: 4px 8px;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
display: block;
margin: 0 auto;
}
.remove-property:hover {
background: #c03537;
.properties-btn-primary:hover {
background: #2196f3;
}
.mcp-property-card {
background: #36393f;
border: 1px solid #40444b;
border-radius: 8px;
padding: 12px;
width: 100%;
cursor: pointer;
transition: all 0.2s ease;
}
.mcp-property-card:hover {
background: #40444b;
border-color: #5865f2;
}
.mcp-property-card:active {
transform: none;
}
.mcp-property-row-label {
display: flex;
align-items: flex-start;
margin-bottom: 6px;
gap: 8px;
}
.mcp-property-label {
color: #b9bbbe;
font-size: 13px;
width: 60px;
flex-shrink: 0;
text-align: left;
}
.mcp-property-value {
color: white;
font-size: 13px;
flex: 1;
word-break: break-all;
}
.mcp-property-required-badge {
color: #f44336;
font-size: 12px;
}
.mcp-property-row-action {
display: flex;
justify-content: flex-end;
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #40444b;
}
.mcp-property-delete-btn {
background: #f44336;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: background-color 0.2s;
}
.mcp-property-delete-btn:hover {
background: #d32f2f;
}
.mcp-property-card-optional {
background: #4f545c;
color: #b9bbbe;
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
}
.property-modal {
max-width: 450px;
}
/* ==================== 音频可视化器样式 ==================== */
@@ -1125,4 +1381,41 @@ body {
.modal-body::-webkit-scrollbar-thumb:hover,
.mcp-tools-list::-webkit-scrollbar-thumb:hover {
background: #4f545c;
}
/* ==================== 摄像头显示区域样式 ==================== */
.camera-container {
position: fixed;
top: 20px;
left: 20px;
width: 240px;
height: 180px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
z-index: 1000;
display: none;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
cursor: move;
}
.camera-container.active {
display: block;
}
.camera-container.dragging {
cursor: grabbing;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: rgba(88, 101, 242, 0.5);
}
#cameraVideo {
width: 100%;
height: 100%;
object-fit: cover;
background: #1a1a1a;
pointer-events: none;
transform: scaleX(-1);
}
+215
View File
@@ -6,12 +6,25 @@ import { initMcpTools } from './core/mcp/tools.js?v=0127';
import { uiController } from './ui/controller.js?v=0127';
import { log } from './utils/logger.js?v=0127';
// 辅助函数:将Base64数据转换为Blob
function dataURItoBlob(dataURI) {
const byteString = atob(dataURI.split(',')[1]);
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
// 应用类
class App {
constructor() {
this.uiController = null;
this.audioPlayer = null;
this.live2dManager = null;
this.cameraStream = null;
}
// 初始化应用
@@ -31,8 +44,12 @@ class App {
initMcpTools();
// 检查麦克风可用性
await this.checkMicrophoneAvailability();
// 检查摄像头可用性
this.checkCameraAvailability();
// 初始化Live2D
await this.initLive2D();
// 初始化摄像头
this.initCamera();
// 关闭加载loading
this.setModelLoadingStatus(false);
log('应用初始化完成', 'success');
@@ -99,6 +116,204 @@ class App {
}
}
}
// 检查摄像头可用性
checkCameraAvailability() {
window.cameraAvailable = true;
log('摄像头可用性检查完成: 默认已绑定验证码', 'success');
}
// 初始化摄像头
async initCamera() {
const cameraContainer = document.getElementById('cameraContainer');
const cameraVideo = document.getElementById('cameraVideo');
if (!cameraContainer || !cameraVideo) {
log('摄像头元素未找到,跳过初始化', 'warning');
return Promise.resolve(false);
}
let isDragging = false;
let currentX, currentY, initialX, initialY;
let xOffset = 0, yOffset = 0;
cameraContainer.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
cameraContainer.addEventListener('touchstart', dragStart, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', dragEnd);
function dragStart(e) {
if (e.type === 'touchstart') {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
isDragging = true;
cameraContainer.classList.add('dragging');
}
function drag(e) {
if (isDragging) {
e.preventDefault();
if (e.type === 'touchmove') {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX;
yOffset = currentY;
cameraContainer.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
cameraContainer.classList.remove('dragging');
}
return new Promise((resolve) => {
window.startCamera = async () => {
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
log('浏览器不支持摄像头API', 'warning');
return false;
}
log('正在请求摄像头权限...', 'info');
this.cameraStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, facingMode: 'user' },
audio: false
});
cameraVideo.srcObject = this.cameraStream;
cameraContainer.classList.add('active');
log('摄像头已启动', 'success');
return true;
} catch (error) {
log(`启动摄像头失败: ${error.name} - ${error.message}`, 'error');
if (error.name === 'NotAllowedError') {
log('摄像头权限被拒绝,请检查浏览器设置', 'warning');
} else if (error.name === 'NotFoundError') {
log('未找到摄像头设备', 'warning');
} else if (error.name === 'NotReadableError') {
log('摄像头已被其他程序占用', 'warning');
}
return false;
}
};
window.stopCamera = () => {
if (this.cameraStream) {
this.cameraStream.getTracks().forEach(track => track.stop());
this.cameraStream = null;
cameraVideo.srcObject = null;
log('摄像头已关闭', 'info');
}
};
window.takePhoto = (question = '描述一下看到的物品') => {
return new Promise(async (resolve) => {
const canvas = document.createElement('canvas');
const video = cameraVideo;
if (!video || video.readyState !== video.HAVE_ENOUGH_DATA) {
log('无法拍照:摄像头未就绪', 'warning');
resolve({
success: false,
error: '摄像头未就绪,请确保已连接且摄像头已启动'
});
return;
}
canvas.width = video.videoWidth || 320;
canvas.height = video.videoHeight || 240;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const photoData = canvas.toDataURL('image/jpeg', 0.8);
log(`拍照成功,图像数据长度: ${photoData.length}`, 'success');
try {
const xz_tester_vision = localStorage.getItem('xz_tester_vision');
if (xz_tester_vision) {
let visionInfo = null;
try {
visionInfo = JSON.parse(xz_tester_vision);
} catch (err) {
throw new Error(`视觉配置解析失败`);
}
const { url, token } = visionInfo || {};
if (!url || !token) {
throw new Error('视觉分析失败:配置缺少接口地址(url)或令牌(token)');
}
log(`正在发送图片到视觉分析接口: ${url}`, 'info');
const deviceId = document.getElementById('deviceMac')?.value || '';
const clientId = document.getElementById('clientId')?.value || 'web_test_client';
const formData = new FormData();
formData.append('question', question);
formData.append('image', dataURItoBlob(photoData), 'photo.jpg');
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'Device-Id': deviceId,
'Client-Id': clientId,
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const analysisResult = await response.json();
log(`视觉分析完成: ${JSON.stringify(analysisResult).substring(0, 200)}...`, 'success');
resolve({
success: true,
message: question,
photo_data: photoData,
photo_width: canvas.width,
photo_height: canvas.height,
vision_analysis: analysisResult
});
} else {
log('未配置视觉分析服务', 'warning');
}
} catch (error) {
log(`视觉分析失败: ${error.message}`, 'error');
resolve({
success: true,
message: question,
photo_data: photoData,
photo_width: canvas.width,
photo_height: canvas.height,
vision_analysis: {
success: false,
error: error.message,
fallback: '无法连接到视觉分析服务'
}
});
}
});
};
log('摄像头初始化完成', 'success');
resolve(true);
});
}
}
// 创建并启动应用
@@ -1,4 +1,22 @@
[
{
"name": "self_camera_take_photo",
"description": "Take a photo using the device's camera. This tool captures the current camera frame and returns the image data. You MUST call this tool when user asks to 'take a photo', 'what do you see', 'describe what you see', or similar requests. After getting the photo data, describe what you see in the image. Parameter 'question' is optional, defaults to 'describe objects seen'.",
"inputSchema": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "Question to guide the photo analysis, e.g., 'describe the objects seen'. Can be empty."
}
}
},
"mockResponse": {
"success": true,
"photo_data": "base64_image_data",
"response": "Please describe what you see based on the returned photo_data"
}
},
{
"name": "self.get_device_status",
"description": "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
+183 -68
View File
@@ -27,7 +27,16 @@ export async function initMcpTools() {
const savedTools = localStorage.getItem('mcpTools');
if (savedTools) {
try {
mcpTools = JSON.parse(savedTools);
const parsedTools = JSON.parse(savedTools);
// 合并默认工具和用户保存的工具,保留用户自定义的工具
const defaultToolNames = new Set(defaultMcpTools.map(t => t.name));
// 添加默认工具中不存在的新工具
parsedTools.forEach(tool => {
if (!defaultToolNames.has(tool.name)) {
defaultMcpTools.push(tool);
}
});
mcpTools = defaultMcpTools;
} catch (e) {
log('加载MCP工具失败,使用默认工具', 'warning');
mcpTools = [...defaultMcpTools];
@@ -96,94 +105,167 @@ function renderMcpTools() {
*/
function renderMcpProperties() {
const container = document.getElementById('mcpPropertiesContainer');
const emptyState = document.getElementById('mcpEmptyState');
if (!container) {
return; // Container not found, skip rendering
}
if (mcpProperties.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>';
if (emptyState) {
emptyState.style.display = 'block';
}
container.innerHTML = '';
return;
}
if (emptyState) {
emptyState.style.display = 'none';
}
container.innerHTML = mcpProperties.map((prop, index) => `
<div class="mcp-property-item">
<div class="mcp-property-header">
<span class="mcp-property-name">${prop.name}</span>
<button type="button" onclick="window.mcpModule.deleteMcpProperty(${index})"
style="padding: 3px 8px; border: none; border-radius: 3px; background-color: #f44336; color: white; cursor: pointer; font-size: 11px;">
删除
</button>
<div class="mcp-property-card" onclick="window.mcpModule.editMcpProperty(${index})">
<div class="mcp-property-row-label">
<span class="mcp-property-label">参数名称</span>
<span class="mcp-property-value">${prop.name}${prop.required ? ' <span class="mcp-property-required-badge">[必填]</span>' : ''}</span>
</div>
<div class="mcp-property-row">
<div>
<label class="mcp-small-label">参数名称 *</label>
<input type="text" class="mcp-small-input" value="${prop.name}"
onchange="window.mcpModule.updateMcpProperty(${index}, 'name', this.value)" required>
</div>
<div>
<label class="mcp-small-label">数据类型 *</label>
<select class="mcp-small-input" onchange="window.mcpModule.updateMcpProperty(${index}, 'type', this.value)">
<option value="string" ${prop.type === 'string' ? 'selected' : ''}>字符串</option>
<option value="integer" ${prop.type === 'integer' ? 'selected' : ''}>整数</option>
<option value="number" ${prop.type === 'number' ? 'selected' : ''}>数字</option>
<option value="boolean" ${prop.type === 'boolean' ? 'selected' : ''}>布尔值</option>
<option value="array" ${prop.type === 'array' ? 'selected' : ''}>数组</option>
<option value="object" ${prop.type === 'object' ? 'selected' : ''}>对象</option>
</select>
</div>
<div class="mcp-property-row-label">
<span class="mcp-property-label">数据类型</span>
<span class="mcp-property-value">${getTypeLabel(prop.type)}</span>
</div>
${(prop.type === 'integer' || prop.type === 'number') ? `
<div class="mcp-property-row">
<div>
<label class="mcp-small-label">最小值</label>
<input type="number" class="mcp-small-input" value="${prop.minimum !== undefined ? prop.minimum : ''}"
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'minimum', this.value ? parseFloat(this.value) : undefined)">
</div>
<div>
<label class="mcp-small-label">最大值</label>
<input type="number" class="mcp-small-input" value="${prop.maximum !== undefined ? prop.maximum : ''}"
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'maximum', this.value ? parseFloat(this.value) : undefined)">
</div>
<div class="mcp-property-row-label">
<span class="mcp-property-label">描述</span>
<span class="mcp-property-value">${prop.description || '-'}</span>
</div>
` : ''}
<div class="mcp-property-row-full">
<label class="mcp-small-label">参数描述</label>
<input type="text" class="mcp-small-input" value="${prop.description || ''}"
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'description', this.value)">
<div class="mcp-property-row-action">
<button class="mcp-property-delete-btn" onclick="event.stopPropagation(); window.mcpModule.deleteMcpProperty(${index})">删除</button>
</div>
<label class="mcp-checkbox-label">
<input type="checkbox" ${prop.required ? 'checked' : ''}
onchange="window.mcpModule.updateMcpProperty(${index}, 'required', this.checked)">
必填参数
</label>
</div>
`).join('');
}
/**
* 添加参数
* 获取数据类型标签
*/
function addMcpProperty() {
mcpProperties.push({ name: `param_${mcpProperties.length + 1}`, type: 'string', required: false, description: '' });
renderMcpProperties();
function getTypeLabel(type) {
const typeMap = {
'string': '字符串',
'integer': '整数',
'number': '数字',
'boolean': '布尔值',
'array': '数组',
'object': '对象'
};
return typeMap[type] || type;
}
/**
* 更新参数
* 添加参数 - 打开参数编辑模态框
*/
function updateMcpProperty(index, field, value) {
if (field === 'name') {
const isDuplicate = mcpProperties.some((p, i) => i !== index && p.name === value);
if (isDuplicate) {
alert('参数名称已存在,请使用不同的名称');
renderMcpProperties();
return;
function addMcpProperty() {
openPropertyModal();
}
/**
* 编辑参数 - 打开参数编辑模态框
*/
function editMcpProperty(index) {
openPropertyModal(index);
}
/**
* 打开参数编辑模态框
*/
function openPropertyModal(index = null) {
const form = document.getElementById('mcpPropertyForm');
const title = document.getElementById('mcpPropertyModalTitle');
document.getElementById('mcpPropertyIndex').value = index !== null ? index : -1;
if (index !== null) {
const prop = mcpProperties[index];
title.textContent = '编辑参数';
document.getElementById('mcpPropertyName').value = prop.name;
document.getElementById('mcpPropertyType').value = prop.type || 'string';
document.getElementById('mcpPropertyMinimum').value = prop.minimum !== undefined ? prop.minimum : '';
document.getElementById('mcpPropertyMaximum').value = prop.maximum !== undefined ? prop.maximum : '';
document.getElementById('mcpPropertyDescription').value = prop.description || '';
document.getElementById('mcpPropertyRequired').checked = prop.required || false;
} else {
title.textContent = '添加参数';
form.reset();
document.getElementById('mcpPropertyName').value = `param_${mcpProperties.length + 1}`;
document.getElementById('mcpPropertyType').value = 'string';
document.getElementById('mcpPropertyMinimum').value = '';
document.getElementById('mcpPropertyMaximum').value = '';
document.getElementById('mcpPropertyDescription').value = '';
document.getElementById('mcpPropertyRequired').checked = false;
}
updatePropertyRangeVisibility();
document.getElementById('mcpPropertyModal').style.display = 'flex';
}
/**
* 关闭参数编辑模态框
*/
function closePropertyModal() {
document.getElementById('mcpPropertyModal').style.display = 'none';
}
/**
* 更新数值范围输入框的可见性
*/
function updatePropertyRangeVisibility() {
const type = document.getElementById('mcpPropertyType').value;
const rangeGroup = document.getElementById('mcpPropertyRangeGroup');
if (type === 'integer' || type === 'number') {
rangeGroup.style.display = 'block';
} else {
rangeGroup.style.display = 'none';
}
}
/**
* 处理参数表单提交
*/
function handlePropertySubmit(e) {
e.preventDefault();
const index = parseInt(document.getElementById('mcpPropertyIndex').value);
const name = document.getElementById('mcpPropertyName').value.trim();
const type = document.getElementById('mcpPropertyType').value;
const minimum = document.getElementById('mcpPropertyMinimum').value;
const maximum = document.getElementById('mcpPropertyMaximum').value;
const description = document.getElementById('mcpPropertyDescription').value.trim();
const required = document.getElementById('mcpPropertyRequired').checked;
// 检查名称重复
const isDuplicate = mcpProperties.some((p, i) => i !== index && p.name === name);
if (isDuplicate) {
alert('参数名称已存在,请使用不同的名称');
return;
}
const propData = {
name,
type,
description,
required
};
// 数值类型添加范围限制
if (type === 'integer' || type === 'number') {
if (minimum !== '') {
propData.minimum = parseFloat(minimum);
}
if (maximum !== '') {
propData.maximum = parseFloat(maximum);
}
}
mcpProperties[index][field] = value;
if (field === 'type' && value !== 'integer' && value !== 'number') {
delete mcpProperties[index].minimum;
delete mcpProperties[index].maximum;
renderMcpProperties();
if (index >= 0) {
mcpProperties[index] = propData;
} else {
mcpProperties.push(propData);
}
renderMcpProperties();
closePropertyModal();
}
/**
@@ -206,6 +288,14 @@ function setupMcpEventListeners() {
const cancelBtn = document.getElementById('cancelMcpBtn');
const form = document.getElementById('mcpToolForm');
const addPropertyBtn = document.getElementById('addMcpPropertyBtn');
// 参数编辑模态框相关元素
const propertyModal = document.getElementById('mcpPropertyModal');
const closePropertyBtn = document.getElementById('closeMcpPropertyModalBtn');
const cancelPropertyBtn = document.getElementById('cancelMcpPropertyBtn');
const propertyForm = document.getElementById('mcpPropertyForm');
const propertyTypeSelect = document.getElementById('mcpPropertyType');
// Return early if required elements don't exist (e.g., in test environment)
if (!toggleBtn || !panel || !addBtn || !modal || !closeBtn || !cancelBtn || !form || !addPropertyBtn) {
return;
@@ -225,6 +315,17 @@ function setupMcpEventListeners() {
if (e.target === modal) closeMcpModal();
});
form.addEventListener('submit', handleMcpSubmit);
// 参数编辑模态框事件
if (propertyModal && closePropertyBtn && cancelPropertyBtn && propertyForm && propertyTypeSelect) {
closePropertyBtn.addEventListener('click', closePropertyModal);
cancelPropertyBtn.addEventListener('click', closePropertyModal);
propertyModal.addEventListener('click', (e) => {
if (e.target === propertyModal) closePropertyModal();
});
propertyForm.addEventListener('submit', handlePropertySubmit);
propertyTypeSelect.addEventListener('change', updatePropertyRangeVisibility);
}
}
/**
@@ -266,7 +367,7 @@ function openMcpModal(index = null) {
mcpProperties = [];
}
renderMcpProperties();
document.getElementById('mcpToolModal').style.display = 'block';
document.getElementById('mcpToolModal').style.display = 'flex';
}
/**
@@ -392,12 +493,26 @@ export function getMcpTools() {
/**
* 执行工具调用
*/
export function executeMcpTool(toolName, toolArgs) {
export async function executeMcpTool(toolName, toolArgs) {
const tool = mcpTools.find(t => t.name === toolName);
if (!tool) {
log(`未找到工具: ${toolName}`, 'error');
return { success: false, error: `未知工具: ${toolName}` };
}
// 处理拍照工具
if (toolName === 'self_camera_take_photo') {
if (typeof window.takePhoto === 'function') {
const question = toolArgs && toolArgs.question ? toolArgs.question : '描述一下看到的物品';
log(`正在执行拍照: ${question}`, 'info');
const result = await window.takePhoto(question);
return result;
} else {
log('拍照功能不可用', 'warning');
return { success: false, error: '摄像头未启动或不支持拍照功能' };
}
}
// 如果有模拟返回结果,使用它
if (tool.mockResponse) {
// 替换模板变量
@@ -424,4 +539,4 @@ export function executeMcpTool(toolName, toolArgs) {
}
// 暴露全局方法供 HTML 内联事件调用
window.mcpModule = { updateMcpProperty, deleteMcpProperty, editMcpTool, deleteMcpTool };
window.mcpModule = { addMcpProperty, editMcpProperty, deleteMcpProperty, editMcpTool, deleteMcpTool };
@@ -74,6 +74,9 @@ export class WebSocketHandler {
handleTextMessage(message) {
if (message.type === 'hello') {
log(`服务器回应:${JSON.stringify(message, null, 2)}`, 'success');
window.cameraAvailable = true;
log('连接成功,摄像头已可用', 'success');
uiController.updateDialButton(true);
uiController.startAIChatSession();
} else if (message.type === 'tts') {
this.handleTTSMessage(message);
@@ -81,6 +84,23 @@ export class WebSocketHandler {
log(`收到音频控制消息: ${JSON.stringify(message)}`, 'info');
} else if (message.type === 'stt') {
log(`识别结果: ${message.text}`, 'info');
// 检查是否需要绑定设备
if (message.text && (message.text.includes('绑定') || message.text.includes('bind'))) {
log('收到设备绑定提示,更新摄像头状态', 'warning');
window.cameraAvailable = false;
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
// 更新摄像头按钮状态
const cameraBtn = document.getElementById('cameraBtn');
if (cameraBtn) {
cameraBtn.classList.remove('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '摄像头';
cameraBtn.disabled = true;
cameraBtn.title = '请先绑定验证码';
}
}
// 使用新的聊天消息回调显示STT消息
if (this.onChatMessage && message.text) {
this.onChatMessage(message.text, true);
@@ -249,30 +269,57 @@ export class WebSocketHandler {
log(`调用工具: ${toolName} 参数: ${JSON.stringify(toolArgs)}`, 'info');
const result = executeMcpTool(toolName, toolArgs);
const replyMessage = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"result": {
"content": [
{
"type": "text",
"text": JSON.stringify(result)
}
],
"isError": false
executeMcpTool(toolName, toolArgs).then(result => {
const replyMessage = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"result": {
"content": [
{
"type": "text",
"text": JSON.stringify(result)
}
],
"isError": false
}
}
}
});
});
log(`客户端上报: ${replyMessage}`, 'info');
this.websocket.send(replyMessage);
log(`客户端上报: ${replyMessage}`, 'info');
this.websocket.send(replyMessage);
}).catch(error => {
log(`工具执行失败: ${error.message}`, 'error');
const errorReply = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": payload.id,
"error": {
"code": -32603,
"message": error.message
}
}
});
this.websocket.send(errorReply);
});
} else if (payload.method === 'initialize') {
log(`收到工具初始化请求: ${JSON.stringify(payload.params)}`, 'info');
// 保存视觉分析接口地址
const visionUrl = document.getElementById('visionUrl');
const visionConfig = payload?.params?.capabilities?.vision;
if (visionConfig && typeof visionConfig === 'object' && visionConfig.url && visionConfig.token) {
const visionConfigStr = JSON.stringify(visionConfig);
localStorage.setItem('xz_tester_vision', visionConfigStr);
if (visionUrl) visionUrl.value = visionConfig.url;
} else {
localStorage.removeItem('xz_tester_vision');
if (visionUrl) visionUrl.value = '';
}
const replyMessage = JSON.stringify({
"session_id": message.session_id || "",
"type": "mcp",
@@ -387,6 +434,17 @@ export class WebSocketHandler {
const audioRecorder = getAudioRecorder();
audioRecorder.stop();
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
// 隐藏摄像头显示区域
const cameraContainer = document.getElementById('cameraContainer');
if (cameraContainer) {
cameraContainer.classList.remove('active');
}
};
this.websocket.onerror = (error) => {
@@ -420,6 +478,17 @@ export class WebSocketHandler {
this.websocket.close();
const audioRecorder = getAudioRecorder();
audioRecorder.stop();
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
// 隐藏摄像头显示区域
const cameraContainer = document.getElementById('cameraContainer');
if (cameraContainer) {
cameraContainer.classList.remove('active');
}
}
// 发送文本消息
+338 -20
View File
@@ -12,7 +12,42 @@ class Live2DManager {
this.audioContext = null;
this.analyser = null;
this.dataArray = null;
this.lastEmotionActionTime = null; // 上次情绪触发动作的时间
this.lastEmotionActionTime = null;
this.currentModelName = null;
// 模型特定配置
this.modelConfig = {
'hiyori_pro_zh': {
mouthParam: 'ParamMouthOpenY',
mouthAmplitude: 1.0,
mouthThresholds: { low: 0.3, high: 0.7 },
motionMap: {
'FlickUp': 'FlickUp',
'FlickDown': 'FlickDown',
'Tap': 'Tap',
'Tap@Body': 'Tap@Body',
'Flick': 'Flick',
'Flick@Body': 'Flick@Body'
}
},
'natori_pro_zh': {
mouthParam: 'ParamMouthOpenY',
mouthAmplitude: 1.0,
mouthThresholds: { low: 0.1, high: 0.4 },
mouthFormParam: 'ParamMouthForm',
mouthFormAmplitude: 1.0,
mouthForm2Param: 'ParamMouthForm2',
mouthForm2Amplitude: 0.8,
motionMap: {
'FlickUp': 'FlickUp',
'FlickDown': 'Flick@Body',
'Tap': 'Tap',
'Tap@Body': 'Tap@Head',
'Flick': 'Tap',
'Flick@Body': 'Flick@Body'
}
}
};
// 情绪到动作的映射
this.emotionToActionMap = {
@@ -67,10 +102,33 @@ class Live2DManager {
const currentPath = window.location.pathname;
const lastSlashIndex = currentPath.lastIndexOf('/');
const basePath = currentPath.substring(0, lastSlashIndex + 1);
const modelPath = basePath + 'hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json';
// 从 localStorage 读取上次选择的模型,如果没有则使用默认
const savedModelName = localStorage.getItem('live2dModel') || 'hiyori_pro_zh';
const modelFileMap = {
'hiyori_pro_zh': 'hiyori_pro_t11.model3.json',
'natori_pro_zh': 'natori_pro_t06.model3.json'
};
const modelFileName = modelFileMap[savedModelName] || 'hiyori_pro_t11.model3.json';
const modelPath = basePath + 'resources/' + savedModelName + '/runtime/' + modelFileName;
this.live2dModel = await PIXI.live2d.Live2DModel.from(modelPath);
this.live2dApp.stage.addChild(this.live2dModel);
// 保存当前模型名称
this.currentModelName = savedModelName;
// 更新下拉框显示
const modelSelect = document.getElementById('live2dModelSelect');
if (modelSelect) {
modelSelect.value = savedModelName;
}
// 设置模型特定的嘴部参数名
if (this.modelConfig[savedModelName]) {
this.mouthParam = this.modelConfig[savedModelName].mouthParam || 'ParamMouthOpenY';
}
// 设置模型属性
this.live2dModel.scale.set(0.33);
this.live2dModel.x = (window.innerWidth - this.live2dModel.width) * 0.5;
@@ -364,33 +422,88 @@ class Live2DManager {
if (internal && internal.coreModel) {
const coreModel = internal.coreModel;
// 获取音频分贝值
let mouthValue = 0;
let mouthOpenY = 0;
let mouthForm = 0;
let mouthForm2 = 0;
let average = 0;
if (this.analyser && this.dataArray) {
this.analyser.getByteFrequencyData(this.dataArray);
average = this.dataArray.reduce((a, b) => a + b) / this.dataArray.length;
// 优化音量映射函数,使中等音量范围变化更明显
// 使用S形曲线函数,在中等音量范围有更好的响应
const normalizedVolume = average / 255;
// S形曲线:在0.3-0.7范围内有最大的斜率(变化最明显)
if (normalizedVolume < 0.3) {
// 低音量:缓慢增长
mouthValue = Math.pow(normalizedVolume / 0.3, 1.5) * 0.3;
} else if (normalizedVolume < 0.7) {
// 中等音量:线性增长,变化最明显
mouthValue = 0.3 + (normalizedVolume - 0.3) / 0.4 * 0.5;
} else {
// 高音量:缓慢接近最大值
mouthValue = 0.8 + Math.pow((normalizedVolume - 0.7) / 0.3, 1.2) * 0.2;
// 获取模型特定的阈值
let lowThreshold = 0.3;
let highThreshold = 0.7;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
lowThreshold = this.modelConfig[this.currentModelName].mouthThresholds?.low || 0.3;
highThreshold = this.modelConfig[this.currentModelName].mouthThresholds?.high || 0.7;
}
// 确保嘴部参数在0-1范围内
mouthValue = Math.min(Math.max(mouthValue, 0), 1);
// 使用模型特定的阈值进行映射
let minOpenY = 0.1;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
minOpenY = this.modelConfig[this.currentModelName].mouthMinOpenY || 0.1;
}
if (normalizedVolume < lowThreshold) {
mouthOpenY = minOpenY + Math.pow(normalizedVolume / lowThreshold, 1.5) * (0.4 - minOpenY);
} else if (normalizedVolume < highThreshold) {
mouthOpenY = 0.4 + (normalizedVolume - lowThreshold) / (highThreshold - lowThreshold) * 0.4;
} else {
mouthOpenY = 0.8 + Math.pow((normalizedVolume - highThreshold) / (1 - highThreshold), 1.2) * 0.2;
}
// 应用模型特定的嘴部开合幅度
let amplitudeMultiplier = 1.0;
let maxOpenY = 2.5;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
amplitudeMultiplier = this.modelConfig[this.currentModelName].mouthAmplitude;
maxOpenY = this.modelConfig[this.currentModelName].maxOpenY || 2.5;
}
mouthOpenY = mouthOpenY * amplitudeMultiplier;
mouthOpenY = Math.min(Math.max(mouthOpenY, 0), maxOpenY);
// 计算嘴型参数(仅对支持嘴型变化的模型)
if (this.currentModelName && this.modelConfig[this.currentModelName]?.mouthFormParam) {
const config = this.modelConfig[this.currentModelName];
const formAmplitude = config.mouthFormAmplitude || 0.5;
const form2Amplitude = config.mouthForm2Amplitude || 0;
// 嘴型随音量变化:
// 低音量:嘴型偏"一"字(负值)
// 高音量:嘴型偏"o"字(正值)
// 音量=0时:嘴型=0(自然状态)
mouthForm = (normalizedVolume - 0.5) * 2 * formAmplitude;
mouthForm = Math.max(-formAmplitude, Math.min(formAmplitude, mouthForm));
// 第二嘴型参数(natori特有)
if (config.mouthForm2Param) {
mouthForm2 = (normalizedVolume - 0.3) * 2 * form2Amplitude;
mouthForm2 = Math.max(-form2Amplitude, Math.min(form2Amplitude, mouthForm2));
}
}
// 调试日志:输出嘴部参数
console.log(`[Live2D] 模型: ${this.currentModelName || 'unknown'}, 音量: ${average?.toFixed(0)}, OpenY: ${mouthOpenY.toFixed(3)}, Form: ${mouthForm.toFixed(3)}, Form2: ${mouthForm2.toFixed(3)}`);
}
coreModel.setParameterValueById(this.mouthParam, mouthValue);
// 设置嘴部开合参数
coreModel.setParameterValueById(this.mouthParam, mouthOpenY);
// 设置嘴型参数(仅对支持嘴型变化的模型)
if (this.currentModelName && this.modelConfig[this.currentModelName]?.mouthFormParam) {
const config = this.modelConfig[this.currentModelName];
const formParam = config.mouthFormParam;
coreModel.setParameterValueById(formParam, mouthForm);
// 设置第二嘴型参数(natori特有)
if (config.mouthForm2Param) {
coreModel.setParameterValueById(config.mouthForm2Param, mouthForm2);
}
}
coreModel.update();
}
this.mouthAnimationId = requestAnimationFrame(() => this.animateMouth());
@@ -473,12 +586,129 @@ class Live2DManager {
motion(name) {
try {
if (!this.live2dModel) return;
this.live2dModel.motion(name);
// 根据当前模型获取对应的动作名称
let actualMotionName = name;
if (this.currentModelName && this.modelConfig[this.currentModelName]) {
const motionMap = this.modelConfig[this.currentModelName].motionMap;
actualMotionName = motionMap[name] || name;
}
this.live2dModel.motion(actualMotionName);
} catch (error) {
console.error('触发动作失败:', error);
}
}
/**
* 设置模型交互事件
*/
setupModelInteractions() {
if (!this.live2dModel) return;
this.live2dModel.interactive = true;
this.live2dModel.on('doublehit', (args) => {
const area = Array.isArray(args) ? args[0] : args;
if (area === 'Body') {
this.motion('Flick@Body');
} else if (area === 'Head' || area === 'Face') {
this.motion('Flick');
}
const app = window.chatApp;
const payload = JSON.stringify({ type: 'live2d', event: 'doublehit', area });
if (app && app.dataChannel && app.dataChannel.readyState === 'open') {
app.dataChannel.send(payload);
}
});
this.live2dModel.on('singlehit', (args) => {
const area = Array.isArray(args) ? args[0] : args;
if (area === 'Body') {
this.motion('Tap@Body');
} else if (area === 'Head' || area === 'Face') {
this.motion('Tap');
}
const app = window.chatApp;
const payload = JSON.stringify({ type: 'live2d', event: 'singlehit', area });
if (app && app.dataChannel && app.dataChannel.readyState === 'open') {
app.dataChannel.send(payload);
}
});
this.live2dModel.on('swipe', (args) => {
const area = Array.isArray(args) ? args[0] : args;
const dir = Array.isArray(args) ? args[1] : undefined;
if (area === 'Body') {
if (dir === 'up') {
this.motion('FlickUp');
} else if (dir === 'down') {
this.motion('FlickDown');
}
}
const app = window.chatApp;
const payload = JSON.stringify({ type: 'live2d', event: 'swipe', area, dir });
if (app && app.dataChannel && app.dataChannel.readyState === 'open') {
app.dataChannel.send(payload);
}
});
this.live2dModel.on('pointerdown', (event) => {
try {
const global = event.data.global;
const bounds = this.live2dModel.getBounds();
if (!bounds || !bounds.contains(global.x, global.y)) return;
const relX = (global.x - bounds.x) / (bounds.width || 1);
const relY = (global.y - bounds.y) / (bounds.height || 1);
let area = '';
if (relX >= 0.4 && relX <= 0.6) {
if (relY <= 0.15) {
area = 'Head';
} else if (relY >= 0.7) {
area = 'Body';
}
}
if (!area) return;
const now = Date.now();
const dt = now - (this._lastClickTime || 0);
const dx = global.x - (this._lastClickPos?.x || 0);
const dy = global.y - (this._lastClickPos?.y || 0);
const dist = Math.hypot(dx, dy);
if (this._lastClickTime && dt <= this._doubleClickMs && dist <= this._doubleClickDist) {
if (this._singleClickTimer) {
clearTimeout(this._singleClickTimer);
this._singleClickTimer = null;
}
this.live2dModel.emit('doublehit', area);
this._lastClickTime = null;
this._lastClickPos = null;
} else {
this._lastClickTime = now;
this._lastClickPos = { x: global.x, y: global.y };
this._singleClickTimer = setTimeout(() => {
this._singleClickTimer = null;
this.live2dModel.emit('singlehit', area);
}, this._doubleClickMs);
}
} catch (e) {
console.warn('pointerdown 处理出错:', e);
}
});
}
/**
* 清理资源
*/
@@ -500,6 +730,94 @@ class Live2DManager {
}
this.live2dModel = null;
}
/**
* 切换 Live2D 模型
* @param {string} modelName - 模型目录名称,如 'hiyori_pro_zh'、'natori_pro_zh'
* @returns {Promise<boolean>} - 切换是否成功
*/
async switchModel(modelName) {
try {
// 获取模型文件名映射
const modelFileMap = {
'hiyori_pro_zh': 'hiyori_pro_t11.model3.json',
'natori_pro_zh': 'natori_pro_t06.model3.json',
'chitose': 'chitose.model3.json',
'haru_greeter_pro_jp': 'haru_greeter_t05.model3.json'
};
const modelFileName = modelFileMap[modelName];
if (!modelFileName) {
console.error('未知的模型名称:', modelName);
return false;
}
// 获取基础路径
const currentPath = window.location.pathname;
const lastSlashIndex = currentPath.lastIndexOf('/');
const basePath = currentPath.substring(0, lastSlashIndex + 1);
const modelPath = basePath + 'resources/' + modelName + '/runtime/' + modelFileName;
// 如果已存在模型,先移除
if (this.live2dModel) {
this.live2dApp.stage.removeChild(this.live2dModel);
this.live2dModel.destroy();
this.live2dModel = null;
}
// 显示加载状态
const app = window.chatApp;
if (app) {
app.setModelLoadingStatus(true);
}
// 加载新模型
this.live2dModel = await PIXI.live2d.Live2DModel.from(modelPath);
this.live2dApp.stage.addChild(this.live2dModel);
// 设置模型属性
this.live2dModel.scale.set(0.33);
this.live2dModel.x = (window.innerWidth - this.live2dModel.width) * 0.5;
this.live2dModel.y = -50;
// 重新绑定交互事件
this.setupModelInteractions();
// 隐藏加载状态
if (app) {
app.setModelLoadingStatus(false);
}
// 保存当前模型名称
this.currentModelName = modelName;
// 设置模型特定的嘴部参数名
if (this.modelConfig[modelName]) {
this.mouthParam = this.modelConfig[modelName].mouthParam || 'ParamMouthOpenY';
}
// 保存到 localStorage
localStorage.setItem('live2dModel', modelName);
// 更新下拉框显示
const modelSelect = document.getElementById('live2dModelSelect');
if (modelSelect) {
modelSelect.value = modelName;
}
console.log('模型切换成功:', modelName);
return true;
} catch (error) {
console.error('切换模型失败:', error);
const app = window.chatApp;
if (app) {
app.setModelLoadingStatus(false);
}
return false;
}
}
}
// 导出全局实例
+173 -19
View File
@@ -11,8 +11,9 @@ class UIController {
this.visualizerCanvas = null;
this.visualizerContext = null;
this.audioStatsTimer = null;
this.currentBackgroundIndex = 0;
this.currentBackgroundIndex = localStorage.getItem('backgroundIndex') ? parseInt(localStorage.getItem('backgroundIndex')) : 0;
this.backgroundImages = ['1.png', '2.png', '3.png'];
this.dialBtnDisabled = false;
// Bind methods
this.init = this.init.bind(this);
@@ -20,6 +21,7 @@ class UIController {
this.updateDialButton = this.updateDialButton.bind(this);
this.addChatMessage = this.addChatMessage.bind(this);
this.switchBackground = this.switchBackground.bind(this);
this.switchLive2DModel = this.switchLive2DModel.bind(this);
this.showModal = this.showModal.bind(this);
this.hideModal = this.hideModal.bind(this);
this.switchTab = this.switchTab.bind(this);
@@ -51,6 +53,12 @@ class UIController {
// Initialize status display
this.updateConnectionUI(false);
// Apply saved background
const backgroundContainer = document.querySelector('.background-container');
if (backgroundContainer) {
backgroundContainer.style.backgroundImage = `url('./images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
}
this.updateDialButton(false);
console.log('UIController init completed');
@@ -82,10 +90,25 @@ class UIController {
backgroundBtn.addEventListener('click', this.switchBackground);
}
// Model select change event
const modelSelect = document.getElementById('live2dModelSelect');
if (modelSelect) {
modelSelect.addEventListener('change', () => {
this.switchLive2DModel();
});
}
// Dial button
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
dialBtn.addEventListener('click', () => {
dialBtn.disabled = true;
this.dialBtnDisabled = true;
setTimeout(() => {
dialBtn.disabled = false;
this.dialBtnDisabled = false;
}, 3000);
const wsHandler = getWebSocketHandler();
const isConnected = wsHandler.isConnected();
@@ -110,26 +133,80 @@ class UIController {
});
}
// Camera button
const cameraBtn = document.getElementById('cameraBtn');
let cameraTimer = null;
if (cameraBtn) {
cameraBtn.addEventListener('click', () => {
if (cameraTimer) {
clearTimeout(cameraTimer);
cameraTimer = null;
}
cameraTimer = setTimeout(() => {
const cameraContainer = document.getElementById('cameraContainer');
if (!cameraContainer) {
log('摄像头容器不存在', 'warning');
return;
}
const isActive = cameraContainer.classList.contains('active');
if (isActive) {
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
cameraContainer.classList.remove('active');
cameraBtn.classList.remove('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '摄像头';
log('摄像头已关闭', 'info');
} else {
// 打开摄像头
if (typeof window.startCamera === 'function') {
window.startCamera().then(success => {
if (success) {
cameraBtn.classList.add('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '关闭';
} else {
this.addChatMessage('⚠️ 摄像头启动失败,请检查浏览器权限', false);
}
}).catch(error => {
log(`启动摄像头异常: ${error.message}`, 'error');
});
} else {
log('startCamera函数未定义', 'warning');
}
}
}, 300);
});
}
// Record button
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
let recordTimer = null;
recordBtn.addEventListener('click', () => {
const audioRecorder = getAudioRecorder();
if (audioRecorder.isRecording) {
audioRecorder.stop();
// Restore record button to normal state
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
} else {
// Update button state to recording
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
// Start recording, update button state after delay
setTimeout(() => {
audioRecorder.start();
}, 100);
if (recordTimer) {
clearTimeout(recordTimer);
recordTimer = null;
}
recordTimer = setTimeout(() => {
const audioRecorder = getAudioRecorder();
if (audioRecorder.isRecording) {
audioRecorder.stop();
// Restore record button to normal state
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
} else {
// Update button state to recording
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
// Start recording, update button state after delay
setTimeout(() => {
audioRecorder.start();
}, 100);
}
}, 300);
});
}
@@ -220,6 +297,7 @@ class UIController {
updateDialButton(isConnected) {
const dialBtn = document.getElementById('dialBtn');
const recordBtn = document.getElementById('recordBtn');
const cameraBtn = document.getElementById('cameraBtn');
if (dialBtn) {
if (isConnected) {
@@ -239,6 +317,33 @@ class UIController {
}
}
// Update camera button state - reset to default when disconnected
if (cameraBtn && !isConnected) {
const cameraContainer = document.getElementById('cameraContainer');
if (cameraContainer && cameraContainer.classList.contains('active')) {
cameraContainer.classList.remove('active');
}
cameraBtn.classList.remove('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '摄像头';
cameraBtn.disabled = true;
cameraBtn.title = '请先连接服务器';
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
window.stopCamera();
}
}
// Update camera button state - enable when connected and camera is available
if (cameraBtn && isConnected) {
if (window.cameraAvailable) {
cameraBtn.disabled = false;
cameraBtn.title = '打开/关闭摄像头';
} else {
cameraBtn.disabled = true;
cameraBtn.title = '请先绑定验证码';
}
}
// Update record button state
if (recordBtn) {
const microphoneAvailable = window.microphoneAvailable !== false;
@@ -324,6 +429,36 @@ class UIController {
if (backgroundContainer) {
backgroundContainer.style.backgroundImage = `url('./images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
}
localStorage.setItem('backgroundIndex', this.currentBackgroundIndex);
}
// Switch Live2D model
switchLive2DModel() {
const modelSelect = document.getElementById('live2dModelSelect');
if (!modelSelect) {
console.error('模型选择下拉框不存在');
return;
}
const selectedModel = modelSelect.value;
const app = window.chatApp;
if (app && app.live2dManager) {
app.live2dManager.switchModel(selectedModel)
.then(success => {
if (success) {
this.addChatMessage(`已切换到模型: ${selectedModel}`, false);
} else {
this.addChatMessage('模型切换失败', false);
}
})
.catch(error => {
console.error('模型切换错误:', error);
this.addChatMessage('模型切换出错', false);
});
} else {
this.addChatMessage('Live2D管理器未初始化', false);
}
}
// Show modal
@@ -379,6 +514,22 @@ class UIController {
recordBtn.click();
}
}
// Start camera only if camera is available (bound with verification code)
if (window.cameraAvailable && typeof window.startCamera === 'function') {
window.startCamera().then(success => {
if (success) {
const cameraBtn = document.getElementById('cameraBtn');
if (cameraBtn) {
cameraBtn.classList.add('camera-active');
cameraBtn.querySelector('.btn-text').textContent = '关闭';
}
} else {
this.addChatMessage('⚠️ 摄像头启动失败,可能被浏览器拒绝', false);
}
}).catch(error => {
log(`启动摄像头异常: ${error.message}`, 'error');
});
}
}
// Handle connect button click
@@ -468,13 +619,14 @@ class UIController {
// Update dial button state
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
dialBtn.disabled = false;
if (!this.dialBtnDisabled) {
dialBtn.disabled = false;
}
dialBtn.querySelector('.btn-text').textContent = '挂断';
dialBtn.classList.add('dial-active');
}
this.hideModal('settingsModal');
} else {
throw new Error('OTA连接失败');
}
@@ -495,7 +647,9 @@ class UIController {
// Restore dial button state
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
dialBtn.disabled = false;
if (!this.dialBtnDisabled) {
dialBtn.disabled = false;
}
dialBtn.querySelector('.btn-text').textContent = '拨号';
dialBtn.classList.remove('dial-active');
console.log('Dial button state restored successfully');
@@ -0,0 +1,79 @@
============================================================
示例模型
名执 尽 - PRO
============================================================
 该示例可用于学习商用的游戏和App中常用到的手臂切换等难度较高的制作方法。
 该模型角色由画师先崎真琴设计。
 模型的手臂通过渐变实现不同部件间的切换,这种制作方法经常应用于商用的游戏和App中。
 通过该模型,可以学习部件的切换来实现更丰富的动画。
 另外,可以通过Cubism3 Viewer (for OW)来查看嵌入式文件组的动画,了解完整的工作流程中使用到的数据结构。
------------------------------
素材使用许可
------------------------------
 普通用户以及小规模企业在同意授权协议的情况下可用于商业用途。
 中/大规模的企业只能用于非公开的内部试用。
 在使用该素材时,请确认以下的【无偿提供素材使用授权协议】中的“授权类型”、“Live2D原创角色”等的相关内容,
 并必须接受【Live2D Cubism 示例模型的使用授权要求】中的利用条件。
 有关许可证的更多信息,请参阅以下页面。
 https://www.live2d.com/zh-CHS/download/sample-data/
------------------------------
创作者
------------------------------
 插画:先崎 真琴【http://senzakimakoto.com/】
 配音:小野友树【https://web.onoyuki.com/】
 (※配音数据的发布为限定发布,已于2018/06/04结束。)
 模型:Live2D
------------------------------
素材内容
------------------------------
 模型文件(cmo3) ※包含物理模拟的设定
 表情动画文件(can3)
 基本动画文件(can3)
 嵌入文件组(runtime文件夹)
・模型数据(moc3)
・表情数据(exp3.json)
・动作数据(motion3.json)
・模型设定文件(model3.json)
・姿势设定文件(pose3.json)
・物理模拟设定文件(physics3.json)
・辅助显示的文件(cdi3.json)
------------------------------
更新记录
------------------------------
 ※配音数据的发布已于2018/06/04结束。
【cmo3】
 natori_pro_t06.cmo3
 2021年6月10日 公开
【can3】
 natori_pro_exp_t03.can3
 2021年6月10日 公开
【can3】
 natori_pro_motions_t03.can3
 2021年6月10日 公开
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": -0.4,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": -0.4,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": -2,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0.3,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 3,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 3,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0.2,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": -0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0.1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,120 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeLSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeLForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeROpen",
"Value": -1,
"Blend": "Add"
},
{
"Id": "ParamEyeRSmile",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeRForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamEyeBallForm",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRY",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRX",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRAngle",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowLForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm",
"Value": 1,
"Blend": "Add"
},
{
"Id": "ParamBrowRForm2",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamTeethOn",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamCheek",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGlassUD",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassWhite",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlight",
"Value": 0,
"Blend": "Add"
},
{
"Id": "ParamGrassHighlightMove",
"Value": 0,
"Blend": "Add"
}
]
}
@@ -0,0 +1,44 @@
{
"Type": "Live2D Pose",
"FadeInTime": 0.2,
"Groups": [
[
{
"Id": "PartArmAL",
"Link": []
},
{
"Id": "PartArmCL",
"Link": []
},
{
"Id": "PartArmDL",
"Link": []
}
],
[
{
"Id": "PartArmAR",
"Link": []
},
{
"Id": "PartArmBR",
"Link": []
},
{
"Id": "PartArmER",
"Link": []
}
],
[
{
"Id": "PartWatchA",
"Link": []
},
{
"Id": "PartWatchB",
"Link": []
}
]
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

@@ -0,0 +1,711 @@
{
"Version": 3,
"Parameters": [
{
"Id": "ParamAngleX",
"GroupId": "",
"Name": "角度 X"
},
{
"Id": "ParamAngleY",
"GroupId": "",
"Name": "角度 Y"
},
{
"Id": "ParamAngleZ",
"GroupId": "",
"Name": "角度 Z"
},
{
"Id": "ParamEyeLOpen",
"GroupId": "ParamGroupExpression",
"Name": "左眼 开闭"
},
{
"Id": "ParamEyeLSmile",
"GroupId": "ParamGroupExpression",
"Name": "左眼 微笑"
},
{
"Id": "ParamEyeLForm",
"GroupId": "ParamGroupExpression",
"Name": "左眼 变形"
},
{
"Id": "ParamEyeROpen",
"GroupId": "ParamGroupExpression",
"Name": "右眼 开闭"
},
{
"Id": "ParamEyeRSmile",
"GroupId": "ParamGroupExpression",
"Name": "右眼 微笑"
},
{
"Id": "ParamEyeRForm",
"GroupId": "ParamGroupExpression",
"Name": "右眼 变形"
},
{
"Id": "ParamEyeBallX",
"GroupId": "ParamGroupExpression",
"Name": "眼珠 X"
},
{
"Id": "ParamEyeBallY",
"GroupId": "ParamGroupExpression",
"Name": "眼珠 Y"
},
{
"Id": "ParamEyeBallForm",
"GroupId": "ParamGroupExpression",
"Name": "眼珠 缩放"
},
{
"Id": "ParamBrowLY",
"GroupId": "ParamGroupExpression",
"Name": "左眉 上下"
},
{
"Id": "ParamBrowRY",
"GroupId": "ParamGroupExpression",
"Name": "右眉 上下"
},
{
"Id": "ParamBrowLX",
"GroupId": "ParamGroupExpression",
"Name": "左眉 左右"
},
{
"Id": "ParamBrowRX",
"GroupId": "ParamGroupExpression",
"Name": "右眉 左右"
},
{
"Id": "ParamBrowLAngle",
"GroupId": "ParamGroupExpression",
"Name": "左眉 角度"
},
{
"Id": "ParamBrowRAngle",
"GroupId": "ParamGroupExpression",
"Name": "右眉 角度"
},
{
"Id": "ParamBrowLForm",
"GroupId": "ParamGroupExpression",
"Name": "左眉 变形"
},
{
"Id": "ParamBrowLForm2",
"GroupId": "ParamGroupExpression",
"Name": "左眉 变形2"
},
{
"Id": "ParamBrowRForm",
"GroupId": "ParamGroupExpression",
"Name": "右眉 变形"
},
{
"Id": "ParamBrowRForm2",
"GroupId": "ParamGroupExpression",
"Name": "右眉 变形2"
},
{
"Id": "ParamMouthForm",
"GroupId": "ParamGroupExpression",
"Name": "嘴 变形"
},
{
"Id": "ParamMouthOpenY",
"GroupId": "ParamGroupExpression",
"Name": "嘴 开闭"
},
{
"Id": "ParamMouthForm2",
"GroupId": "ParamGroupExpression",
"Name": "嘴 变形2"
},
{
"Id": "ParamTeethOn",
"GroupId": "ParamGroupExpression",
"Name": "牙齿的显示"
},
{
"Id": "ParamCheek",
"GroupId": "ParamGroupExpression",
"Name": "害羞"
},
{
"Id": "ParamGlassUD",
"GroupId": "ParamGroupExpression",
"Name": "眼镜 上下"
},
{
"Id": "ParamGrassWhite",
"GroupId": "ParamGroupExpression",
"Name": "镜片 白"
},
{
"Id": "ParamGrassHighlight",
"GroupId": "ParamGroupExpression",
"Name": "镜片 反光显示"
},
{
"Id": "ParamGrassHighlightMove",
"GroupId": "ParamGroupExpression",
"Name": "镜片 反光移动"
},
{
"Id": "ParamBodyAngleX",
"GroupId": "ParamGroupBody",
"Name": "身体的旋转 X"
},
{
"Id": "ParamBodyAngleY",
"GroupId": "ParamGroupBody",
"Name": "身体的旋转 Y"
},
{
"Id": "ParamBodyAngleZ",
"GroupId": "ParamGroupBody",
"Name": "身体的旋转 Z"
},
{
"Id": "ParamWaistAngleZ",
"GroupId": "ParamGroupBody",
"Name": "腰的旋转 Z"
},
{
"Id": "ParamBodyPosition",
"GroupId": "ParamGroupBody",
"Name": "身体的前后"
},
{
"Id": "ParamBreath",
"GroupId": "ParamGroupBody",
"Name": "呼吸"
},
{
"Id": "ParamLeftShoulderUp",
"GroupId": "ParamGroupBody",
"Name": "左肩的上下"
},
{
"Id": "ParamRightShoulderUp",
"GroupId": "ParamGroupBody",
"Name": "右肩的上下"
},
{
"Id": "ParamAllX",
"GroupId": "ParamGroup",
"Name": "整体的移动 X"
},
{
"Id": "ParamAllY",
"GroupId": "ParamGroup",
"Name": "整体的移动 Y"
},
{
"Id": "ParamAllRotate",
"GroupId": "ParamGroup",
"Name": "整体的旋转"
},
{
"Id": "ParamHairFront",
"GroupId": "ParamGroupSway",
"Name": "头发摇动 前"
},
{
"Id": "ParamHairSide",
"GroupId": "ParamGroupSway",
"Name": "头发摇动 侧"
},
{
"Id": "ParamHairBack",
"GroupId": "ParamGroupSway",
"Name": "头发摇动 后"
},
{
"Id": "ParamHairFrontFuwa",
"GroupId": "ParamGroupSway",
"Name": "前发 蓬松"
},
{
"Id": "ParamHairSideFuwa",
"GroupId": "ParamGroupSway",
"Name": "侧发 蓬松"
},
{
"Id": "ParamHairBackFuwa",
"GroupId": "ParamGroupSway",
"Name": "后发 蓬松"
},
{
"Id": "ParamJacket",
"GroupId": "ParamGroupSway",
"Name": "燕尾的摇动"
},
{
"Id": "ParamChainWaist",
"GroupId": "ParamGroupSway",
"Name": "表链A的摇动"
},
{
"Id": "ParamWatchSwingA1",
"GroupId": "ParamGroupSway",
"Name": "怀表A 摇动1"
},
{
"Id": "ParamWatchSwingA2",
"GroupId": "ParamGroupSway",
"Name": "怀表A 摇动2"
},
{
"Id": "ParamWatchBChain",
"GroupId": "ParamGroupSway",
"Name": "怀表B 表链的摇动"
},
{
"Id": "ParamWatchAX",
"GroupId": "ParamGroup8",
"Name": "怀表A 横向旋转"
},
{
"Id": "ParamWatchBSwitch",
"GroupId": "ParamGroup9",
"Name": "怀表B 开关"
},
{
"Id": "ParamWatchBOpen",
"GroupId": "ParamGroup9",
"Name": "怀表B 表盖的开闭"
},
{
"Id": "ParamWatchBOpen2",
"GroupId": "ParamGroup9",
"Name": "怀表B 表盘的开闭"
},
{
"Id": "ParamWatchBX",
"GroupId": "ParamGroup9",
"Name": "怀表B 横向旋转"
},
{
"Id": "ParamWatchBRoll",
"GroupId": "ParamGroup9",
"Name": "怀表B 旋转"
},
{
"Id": "ParamWatchBLR",
"GroupId": "ParamGroup9",
"Name": "怀表B 左右"
},
{
"Id": "ParamWatchBUD",
"GroupId": "ParamGroup9",
"Name": "怀表B 上下"
},
{
"Id": "ParamArmAL01",
"GroupId": "ParamGroup3",
"Name": "左手臂A 肩"
},
{
"Id": "ParamArmAL02",
"GroupId": "ParamGroup3",
"Name": "左肩A 手肘旋转"
},
{
"Id": "ParamArmAL03",
"GroupId": "ParamGroup3",
"Name": "左手臂A 手腕"
},
{
"Id": "ParamArmAL04",
"GroupId": "ParamGroup3",
"Name": "左手臂A 前臂的前后"
},
{
"Id": "ParamArmAR01",
"GroupId": "ParamGroup2",
"Name": "右手臂A 肩的旋转"
},
{
"Id": "ParamArmAR02",
"GroupId": "ParamGroup2",
"Name": "右手臂A 手肘的旋转"
},
{
"Id": "ParamArmAR03",
"GroupId": "ParamGroup2",
"Name": "右手臂A 手腕的旋转"
},
{
"Id": "ParamArmAR04",
"GroupId": "ParamGroup2",
"Name": "右手臂A 前臂的前后"
},
{
"Id": "ParamArmBR01",
"GroupId": "ParamGroup4",
"Name": "右手臂B 肩的旋转"
},
{
"Id": "ParamArmBR02",
"GroupId": "ParamGroup4",
"Name": "右手臂B 手肘的旋转"
},
{
"Id": "ParamArmBR03",
"GroupId": "ParamGroup4",
"Name": "右手臂B 手腕的旋转"
},
{
"Id": "ParamArmBRHand01",
"GroupId": "ParamGroup4",
"Name": "右手01 显示"
},
{
"Id": "ParamArmBRHand01Roll",
"GroupId": "ParamGroup4",
"Name": "右手01 手指弯曲"
},
{
"Id": "ParamArmBRHand05",
"GroupId": "ParamGroup4",
"Name": "右手05 显示"
},
{
"Id": "ParamArmBRHand05Roll1",
"GroupId": "ParamGroup4",
"Name": "右手05 手指弯曲1"
},
{
"Id": "ParamArmBRHand05Roll2",
"GroupId": "ParamGroup4",
"Name": "右手05 手指弯曲2"
},
{
"Id": "ParamArmBRHand05Roll3",
"GroupId": "ParamGroup4",
"Name": "右手05 手指弯曲3"
},
{
"Id": "ParamArmCR01",
"GroupId": "ParamGroup5",
"Name": "左手臂C 肩的旋转"
},
{
"Id": "ParamArmCR02",
"GroupId": "ParamGroup5",
"Name": "左手臂C 手肘的旋转"
},
{
"Id": "ParamArmCR03",
"GroupId": "ParamGroup5",
"Name": "左手臂C 手腕的旋转"
},
{
"Id": "ParamArmCLHandRoll1",
"GroupId": "ParamGroup5",
"Name": "左手C 手指弯曲1"
},
{
"Id": "ParamArmDL01",
"GroupId": "ParamGroup6",
"Name": "左手臂D 肩的旋转"
},
{
"Id": "ParamArmDL02",
"GroupId": "ParamGroup6",
"Name": "左手臂D 手肘的旋转"
},
{
"Id": "ParamArmDL03",
"GroupId": "ParamGroup6",
"Name": "左手臂D 手腕的旋转"
},
{
"Id": "ParamArmDLHand03Roll",
"GroupId": "ParamGroup6",
"Name": "左手03 手指弯曲"
},
{
"Id": "ParamArmER01",
"GroupId": "ParamGroup7",
"Name": "右手臂E 肩的旋转"
},
{
"Id": "ParamArmER02",
"GroupId": "ParamGroup7",
"Name": "右手臂E 手肘的旋转"
},
{
"Id": "ParamArmER03",
"GroupId": "ParamGroup7",
"Name": "右手臂E 手腕的旋转"
},
{
"Id": "ParamArmER04",
"GroupId": "ParamGroup7",
"Name": "右手臂E 上臂的长度"
},
{
"Id": "ParamArmERHand04",
"GroupId": "ParamGroup7",
"Name": "右手04 显示"
},
{
"Id": "ParamArmERHand04Roll1",
"GroupId": "ParamGroup7",
"Name": "右手04 手指弯曲1"
},
{
"Id": "ParamArmERHand04Roll2",
"GroupId": "ParamGroup7",
"Name": "右手04 手指弯曲2"
},
{
"Id": "ParamArmERHand06",
"GroupId": "ParamGroup7",
"Name": "右手06 显示"
},
{
"Id": "ParamArmERHand06Roll1",
"GroupId": "ParamGroup7",
"Name": "右手06 手指弯曲1"
},
{
"Id": "ParamArmERHand06Roll2",
"GroupId": "ParamGroup7",
"Name": "右手06 手指弯曲2"
}
],
"ParameterGroups": [
{
"Id": "ParamGroupExpression",
"GroupId": "",
"Name": "表情"
},
{
"Id": "ParamGroupBody",
"GroupId": "",
"Name": "身体"
},
{
"Id": "ParamGroup",
"GroupId": "",
"Name": "整体移动"
},
{
"Id": "ParamGroupSway",
"GroupId": "",
"Name": "摇动"
},
{
"Id": "ParamGroup8",
"GroupId": "",
"Name": "怀表A"
},
{
"Id": "ParamGroup9",
"GroupId": "",
"Name": "怀表B"
},
{
"Id": "ParamGroup3",
"GroupId": "",
"Name": "左手臂A"
},
{
"Id": "ParamGroup2",
"GroupId": "",
"Name": "右手臂A"
},
{
"Id": "ParamGroup4",
"GroupId": "",
"Name": "右手臂B"
},
{
"Id": "ParamGroup5",
"GroupId": "",
"Name": "左手臂C"
},
{
"Id": "ParamGroup6",
"GroupId": "",
"Name": "左手臂D"
},
{
"Id": "ParamGroup7",
"GroupId": "",
"Name": "右手臂E"
}
],
"Parts": [
{
"Id": "PartCredit",
"Name": "名牌"
},
{
"Id": "PartCore",
"Name": "CORE"
},
{
"Id": "PartGlass",
"Name": "眼镜"
},
{
"Id": "PartWatchA",
"Name": "怀表A"
},
{
"Id": "PartWatchB",
"Name": "怀表B"
},
{
"Id": "PartHairFront",
"Name": "前发"
},
{
"Id": "PartHead",
"Name": "头"
},
{
"Id": "PartUpperBody",
"Name": "上半身"
},
{
"Id": "PartHairBack",
"Name": "后发"
},
{
"Id": "PartLowerBody",
"Name": "下半身"
},
{
"Id": "PartArmAL",
"Name": "左手臂A"
},
{
"Id": "PartArmAR",
"Name": "右手臂A"
},
{
"Id": "PartArmBR",
"Name": "右手臂B"
},
{
"Id": "PartArmCL",
"Name": "左手臂C"
},
{
"Id": "PartArmDL",
"Name": "左手臂D"
},
{
"Id": "PartArmER",
"Name": "右手臂E"
},
{
"Id": "PartEyeBlow",
"Name": "眉毛"
},
{
"Id": "PartEyeR",
"Name": "右眼"
},
{
"Id": "PartEyeL",
"Name": "左眼"
},
{
"Id": "PartHairLine",
"Name": "发际线"
},
{
"Id": "PartHairShadow",
"Name": "头发阴影"
},
{
"Id": "PartNose",
"Name": "鼻子"
},
{
"Id": "PartMouth",
"Name": "嘴"
},
{
"Id": "PartJacket",
"Name": "燕尾服"
},
{
"Id": "PartArmALFore",
"Name": "左手臂A 前臂"
},
{
"Id": "PartArmARFore",
"Name": "右手臂A 前臂"
},
{
"Id": "PartHand11",
"Name": "手套_1"
},
{
"Id": "PartHand51",
"Name": "手套_5"
},
{
"Id": "PartHand21",
"Name": "手套_2"
},
{
"Id": "PartHand31",
"Name": "手套_3"
},
{
"Id": "PartHand41",
"Name": "手套_4"
},
{
"Id": "PartHand61",
"Name": "手套_6"
}
],
"CombinedParameters": [
[
"ParamAngleX",
"ParamAngleY"
],
[
"ParamEyeBallX",
"ParamEyeBallY"
],
[
"ParamMouthForm",
"ParamMouthOpenY"
],
[
"ParamBodyAngleX",
"ParamBodyAngleY"
],
[
"ParamLeftShoulderUp",
"ParamRightShoulderUp"
],
[
"ParamAllX",
"ParamAllY"
],
[
"ParamWatchSwingA1",
"ParamWatchSwingA2"
],
[
"ParamWatchBLR",
"ParamWatchBUD"
]
]
}

Some files were not shown because too many files have changed in this diff Show More