mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
Merge branch 'main' into py_test_Memory_powermem
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -836,6 +836,7 @@ export default {
|
||||
'modelConfig.unknown': '未知',
|
||||
'modelConfig.isEnabled': '是否启用',
|
||||
'modelConfig.isDefault': '是否默认',
|
||||
'modelConfig.defaultModelCannotDisable': '默认模型配置不允许关闭',
|
||||
'modelConfig.action': '操作',
|
||||
'modelConfig.voiceManagement': '音色管理',
|
||||
'modelConfig.edit': '修改',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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编码的字符串
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.)",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
// 发送文本消息
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 导出全局实例
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
@@ -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日 公开
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1432
File diff suppressed because it is too large
Load Diff
+1971
File diff suppressed because it is too large
Load Diff
+2182
File diff suppressed because it is too large
Load Diff
+2596
File diff suppressed because it is too large
Load Diff
+2316
File diff suppressed because it is too large
Load Diff
+2345
File diff suppressed because it is too large
Load Diff
+2082
File diff suppressed because it is too large
Load Diff
+1907
File diff suppressed because it is too large
Load Diff
@@ -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": []
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
BIN
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"
|
||||
]
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user