mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge pull request #2826 from xinnan-tech/test_page_fix
update:增加live2d模型,修改测试页面样式
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
|
||||
============================================================
|
||||
|
||||
示例模型
|
||||
桃濑日和 - PRO
|
||||
|
||||
============================================================
|
||||
|
||||
该示例时基于Cubism3.0制作的标准模型素材。
|
||||
|
||||
可用于学习变形器的构造以及参数的使用方法。
|
||||
模型的肩部应用了新功能【胶水】。
|
||||
|
||||
|
||||
------------------------------
|
||||
素材使用许可
|
||||
------------------------------
|
||||
|
||||
普通用户以及小规模企业在同意授权协议的情况下可用于商业用途。
|
||||
中/大规模的企业只能用于非公开的内部试用。
|
||||
在使用该素材时,请确认以下的【无偿提供素材使用授权协议】中的“授权类型”、“Live2D原创角色”等的相关内容,
|
||||
并必须接受【Live2D Cubism 示例模型的使用授权要求】中的利用条件。
|
||||
|
||||
有关许可证的更多信息,请参阅以下页面。
|
||||
https://www.live2d.com/zh-CHS/download/sample-data/
|
||||
|
||||
|
||||
------------------------------
|
||||
创作者
|
||||
------------------------------
|
||||
|
||||
插画:Kani Biimu
|
||||
模型:Live2D
|
||||
|
||||
|
||||
------------------------------
|
||||
素材内容
|
||||
------------------------------
|
||||
|
||||
模型文件(cmo3) ※包含物理模拟的设定
|
||||
动画文件(can3)
|
||||
嵌入文件列表(runtime文件夹)
|
||||
・模型数据(moc3)
|
||||
・动作数据(motion3.json)
|
||||
・模型设定文件(model3.json)
|
||||
・物理模拟设定文件(physics3.json)
|
||||
・姿势设定文件(pose3.json)
|
||||
・辅助显示的文件(cdi3.json)
|
||||
|
||||
|
||||
------------------------------
|
||||
更新记录
|
||||
------------------------------
|
||||
|
||||
【cmo3】
|
||||
|
||||
hiyori_pro_t11
|
||||
2023年03月08日 修改了部分模型关键点
|
||||
|
||||
hiyori_pro_t10
|
||||
2021年06月10日 公开
|
||||
|
||||
|
||||
【can3】
|
||||
|
||||
hiyori_pro_t04
|
||||
2023年03月08日 修改了部分动画关键帧
|
||||
|
||||
hiyori_pro_t03
|
||||
2021年06月10日 公开
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "ParamAngleX",
|
||||
"GroupId": "ParamGroupFace",
|
||||
"Name": "角度 X"
|
||||
},
|
||||
{
|
||||
"Id": "ParamAngleY",
|
||||
"GroupId": "ParamGroupFace",
|
||||
"Name": "角度 Y"
|
||||
},
|
||||
{
|
||||
"Id": "ParamAngleZ",
|
||||
"GroupId": "ParamGroupFace",
|
||||
"Name": "角度 Z"
|
||||
},
|
||||
{
|
||||
"Id": "ParamCheek",
|
||||
"GroupId": "ParamGroupFace",
|
||||
"Name": "脸颊泛红"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeLOpen",
|
||||
"GroupId": "ParamGroupEyes",
|
||||
"Name": "左眼 开闭"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeLSmile",
|
||||
"GroupId": "ParamGroupEyes",
|
||||
"Name": "左眼 微笑"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeROpen",
|
||||
"GroupId": "ParamGroupEyes",
|
||||
"Name": "右眼 开闭"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeRSmile",
|
||||
"GroupId": "ParamGroupEyes",
|
||||
"Name": "右眼 微笑"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeBallX",
|
||||
"GroupId": "ParamGroupEyeballs",
|
||||
"Name": "眼珠 X"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeBallY",
|
||||
"GroupId": "ParamGroupEyeballs",
|
||||
"Name": "眼珠 Y"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLY",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "左眉 上下"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRY",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "右眉 上下"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLX",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "左眉 左右"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRX",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "右眉 左右"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLAngle",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "左眉 角度"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRAngle",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "右眉 角度"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLForm",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "左眉 变形"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRForm",
|
||||
"GroupId": "ParamGroupBrows",
|
||||
"Name": "右眉 变形"
|
||||
},
|
||||
{
|
||||
"Id": "ParamMouthForm",
|
||||
"GroupId": "ParamGroupMouth",
|
||||
"Name": "嘴 变形"
|
||||
},
|
||||
{
|
||||
"Id": "ParamMouthOpenY",
|
||||
"GroupId": "ParamGroupMouth",
|
||||
"Name": "嘴 开闭"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBodyAngleX",
|
||||
"GroupId": "ParamGroupBody",
|
||||
"Name": "身体的旋转 X"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBodyAngleY",
|
||||
"GroupId": "ParamGroupBody",
|
||||
"Name": "身体的旋转 Y"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBodyAngleZ",
|
||||
"GroupId": "ParamGroupBody",
|
||||
"Name": "身体的旋转 Z"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBreath",
|
||||
"GroupId": "ParamGroupBody",
|
||||
"Name": "呼吸"
|
||||
},
|
||||
{
|
||||
"Id": "ParamShoulder",
|
||||
"GroupId": "ParamGroupBody",
|
||||
"Name": "肩"
|
||||
},
|
||||
{
|
||||
"Id": "ParamLeg",
|
||||
"GroupId": "ParamGroupBody",
|
||||
"Name": "腿"
|
||||
},
|
||||
{
|
||||
"Id": "ParamArmLA",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "左臂 A"
|
||||
},
|
||||
{
|
||||
"Id": "ParamArmRA",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "右臂 A"
|
||||
},
|
||||
{
|
||||
"Id": "ParamArmLB",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "左臂 B"
|
||||
},
|
||||
{
|
||||
"Id": "ParamArmRB",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "右臂 B"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHandLB",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "左手B 旋转"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHandRB",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "右手B 旋转"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHandL",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "左手"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHandR",
|
||||
"GroupId": "ParamGroupArms",
|
||||
"Name": "右手"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBustY",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "胸部 摇动"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHairAhoge",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "头发摇动 呆毛"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHairFront",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "头发摇动 前"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHairBack",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "头发摇动 后"
|
||||
},
|
||||
{
|
||||
"Id": "ParamSideupRibbon",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "发饰的摇动"
|
||||
},
|
||||
{
|
||||
"Id": "ParamRibbon",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "蝴蝶结的摇动"
|
||||
},
|
||||
{
|
||||
"Id": "ParamSkirt",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "短裙的摇动"
|
||||
},
|
||||
{
|
||||
"Id": "ParamSkirt2",
|
||||
"GroupId": "ParamGroupSway",
|
||||
"Name": "短裙的上卷"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_1_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[0]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_2_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[1]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_3_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[2]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_4_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[3]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_5_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[4]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_6_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[5]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_7_ArtMesh62",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "[6]辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_1_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[0]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_2_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[1]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_3_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[2]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_4_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[3]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_5_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[4]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_6_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[5]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_7_ArtMesh61",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "[6]辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_1_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[0]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_2_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[1]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_3_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[2]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_4_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[3]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_5_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[4]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_6_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[5]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_7_ArtMesh55",
|
||||
"GroupId": "ParamGroup4",
|
||||
"Name": "[6]侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_1_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[0]侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_2_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[1]侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_3_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[2]侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_4_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[3]侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_5_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[4]侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_6_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[5]侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Param_Angle_Rotation_7_ArtMesh54",
|
||||
"GroupId": "ParamGroup3",
|
||||
"Name": "[6]侧发右"
|
||||
}
|
||||
],
|
||||
"ParameterGroups": [
|
||||
{
|
||||
"Id": "ParamGroupFace",
|
||||
"GroupId": "",
|
||||
"Name": "脸"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupEyes",
|
||||
"GroupId": "",
|
||||
"Name": "眼睛"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupEyeballs",
|
||||
"GroupId": "",
|
||||
"Name": "眼珠"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupBrows",
|
||||
"GroupId": "",
|
||||
"Name": "眉毛"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupMouth",
|
||||
"GroupId": "",
|
||||
"Name": "嘴"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupBody",
|
||||
"GroupId": "",
|
||||
"Name": "身体"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupArms",
|
||||
"GroupId": "",
|
||||
"Name": "手臂"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroupSway",
|
||||
"GroupId": "",
|
||||
"Name": "摇动"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroup2",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 辫子左"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroup",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 辫子右"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroup4",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 侧发左"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroup3",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 侧发右"
|
||||
}
|
||||
],
|
||||
"Parts": [
|
||||
{
|
||||
"Id": "PartCore",
|
||||
"Name": "Core"
|
||||
},
|
||||
{
|
||||
"Id": "PartCheek",
|
||||
"Name": "脸颊"
|
||||
},
|
||||
{
|
||||
"Id": "PartBrow",
|
||||
"Name": "眉毛"
|
||||
},
|
||||
{
|
||||
"Id": "PartEye",
|
||||
"Name": "眼睛"
|
||||
},
|
||||
{
|
||||
"Id": "PartNose",
|
||||
"Name": "鼻子"
|
||||
},
|
||||
{
|
||||
"Id": "PartMouth",
|
||||
"Name": "嘴"
|
||||
},
|
||||
{
|
||||
"Id": "PartFace",
|
||||
"Name": "脸"
|
||||
},
|
||||
{
|
||||
"Id": "PartEar",
|
||||
"Name": "耳朵"
|
||||
},
|
||||
{
|
||||
"Id": "PartHairSide",
|
||||
"Name": "侧发"
|
||||
},
|
||||
{
|
||||
"Id": "PartHairFront",
|
||||
"Name": "前发"
|
||||
},
|
||||
{
|
||||
"Id": "PartHairBack",
|
||||
"Name": "后发"
|
||||
},
|
||||
{
|
||||
"Id": "PartNeck",
|
||||
"Name": "脖子"
|
||||
},
|
||||
{
|
||||
"Id": "PartArmA",
|
||||
"Name": "手臂A"
|
||||
},
|
||||
{
|
||||
"Id": "PartArmB",
|
||||
"Name": "手臂B"
|
||||
},
|
||||
{
|
||||
"Id": "PartBody",
|
||||
"Name": "身体"
|
||||
},
|
||||
{
|
||||
"Id": "PartBackground",
|
||||
"Name": "背景"
|
||||
},
|
||||
{
|
||||
"Id": "PartSketch",
|
||||
"Name": "[参考图]"
|
||||
},
|
||||
{
|
||||
"Id": "PartEyeBall",
|
||||
"Name": "眼珠"
|
||||
},
|
||||
{
|
||||
"Id": "ArtMesh55_Skinning",
|
||||
"Name": "侧发右"
|
||||
},
|
||||
{
|
||||
"Id": "Part4",
|
||||
"Name": "侧发左(旋转)"
|
||||
},
|
||||
{
|
||||
"Id": "ArtMesh54_Skinning",
|
||||
"Name": "侧发右(蒙皮)"
|
||||
},
|
||||
{
|
||||
"Id": "Part3",
|
||||
"Name": "侧发右(旋转)"
|
||||
},
|
||||
{
|
||||
"Id": "ArtMesh61_Skinning",
|
||||
"Name": "辫子右(蒙皮)"
|
||||
},
|
||||
{
|
||||
"Id": "Part",
|
||||
"Name": "辫子右(旋转)"
|
||||
},
|
||||
{
|
||||
"Id": "ArtMesh62_Skinning",
|
||||
"Name": "辫子左(蒙皮)"
|
||||
},
|
||||
{
|
||||
"Id": "Part2",
|
||||
"Name": "辫子左(旋转)"
|
||||
}
|
||||
],
|
||||
"CombinedParameters": [
|
||||
[
|
||||
"ParamAngleX",
|
||||
"ParamAngleY"
|
||||
],
|
||||
[
|
||||
"ParamMouthForm",
|
||||
"ParamMouthOpenY"
|
||||
]
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"FileReferences": {
|
||||
"Moc": "hiyori_pro_t11.moc3",
|
||||
"Textures": [
|
||||
"hiyori_pro_t11.2048/texture_00.png",
|
||||
"hiyori_pro_t11.2048/texture_01.png"
|
||||
],
|
||||
"Physics": "hiyori_pro_t11.physics3.json",
|
||||
"Pose": "hiyori_pro_t11.pose3.json",
|
||||
"DisplayInfo": "hiyori_pro_t11.cdi3.json",
|
||||
"Motions": {
|
||||
"Idle": [
|
||||
{
|
||||
"File": "motion/hiyori_m01.motion3.json"
|
||||
},
|
||||
{
|
||||
"File": "motion/hiyori_m02.motion3.json"
|
||||
},
|
||||
{
|
||||
"File": "motion/hiyori_m05.motion3.json"
|
||||
}
|
||||
],
|
||||
"Flick": [
|
||||
{
|
||||
"File": "motion/hiyori_m03.motion3.json"
|
||||
}
|
||||
],
|
||||
"FlickDown": [
|
||||
{
|
||||
"File": "motion/hiyori_m04.motion3.json"
|
||||
}
|
||||
],
|
||||
"FlickUp": [
|
||||
{
|
||||
"File": "motion/hiyori_m06.motion3.json"
|
||||
}
|
||||
],
|
||||
"Tap": [
|
||||
{
|
||||
"File": "motion/hiyori_m07.motion3.json"
|
||||
},
|
||||
{
|
||||
"File": "motion/hiyori_m08.motion3.json"
|
||||
}
|
||||
],
|
||||
"Tap@Body": [
|
||||
{
|
||||
"File": "motion/hiyori_m09.motion3.json"
|
||||
}
|
||||
],
|
||||
"Flick@Body": [
|
||||
{
|
||||
"File": "motion/hiyori_m10.motion3.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Groups": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Name": "LipSync",
|
||||
"Ids": [
|
||||
"ParamMouthOpenY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Name": "EyeBlink",
|
||||
"Ids": [
|
||||
"ParamEyeLOpen",
|
||||
"ParamEyeROpen"
|
||||
]
|
||||
}
|
||||
],
|
||||
"HitAreas": [
|
||||
{
|
||||
"Id": "HitArea",
|
||||
"Name": "Body"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Type": "Live2D Pose",
|
||||
"Groups": [
|
||||
[
|
||||
{
|
||||
"Id": "PartArmA",
|
||||
"Link": []
|
||||
},
|
||||
{
|
||||
"Id": "PartArmB",
|
||||
"Link": []
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,860 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Meta": {
|
||||
"Duration": 4.43,
|
||||
"Fps": 30.0,
|
||||
"Loop": true,
|
||||
"AreBeziersRestricted": false,
|
||||
"CurveCount": 29,
|
||||
"TotalSegmentCount": 106,
|
||||
"TotalPointCount": 287,
|
||||
"UserDataCount": 0,
|
||||
"TotalUserDataSize": 0
|
||||
},
|
||||
"Curves": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleX",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.211,
|
||||
1,
|
||||
0.422,
|
||||
0,
|
||||
0.633,
|
||||
0,
|
||||
1,
|
||||
0.911,
|
||||
0,
|
||||
1.189,
|
||||
5,
|
||||
1.467,
|
||||
5,
|
||||
1,
|
||||
1.689,
|
||||
5,
|
||||
1.911,
|
||||
-16,
|
||||
2.133,
|
||||
-16,
|
||||
1,
|
||||
2.356,
|
||||
-16,
|
||||
2.578,
|
||||
13.871,
|
||||
2.8,
|
||||
13.871,
|
||||
1,
|
||||
2.956,
|
||||
13.871,
|
||||
3.111,
|
||||
0,
|
||||
3.267,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.211,
|
||||
0,
|
||||
0.422,
|
||||
0,
|
||||
0.633,
|
||||
0,
|
||||
1,
|
||||
0.911,
|
||||
0,
|
||||
1.189,
|
||||
-25,
|
||||
1.467,
|
||||
-25,
|
||||
1,
|
||||
1.689,
|
||||
-25,
|
||||
1.911,
|
||||
-15.225,
|
||||
2.133,
|
||||
-11,
|
||||
1,
|
||||
2.356,
|
||||
-6.775,
|
||||
2.578,
|
||||
-5.127,
|
||||
2.8,
|
||||
-2.5,
|
||||
1,
|
||||
2.956,
|
||||
-0.661,
|
||||
3.111,
|
||||
0,
|
||||
3.267,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleZ",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.222,
|
||||
0,
|
||||
0.444,
|
||||
0,
|
||||
0.667,
|
||||
0,
|
||||
1,
|
||||
0.756,
|
||||
0,
|
||||
0.844,
|
||||
-4,
|
||||
0.933,
|
||||
-4,
|
||||
1,
|
||||
1.122,
|
||||
-4,
|
||||
1.311,
|
||||
18,
|
||||
1.5,
|
||||
18,
|
||||
1,
|
||||
1.722,
|
||||
18,
|
||||
1.944,
|
||||
-14,
|
||||
2.167,
|
||||
-14,
|
||||
1,
|
||||
2.567,
|
||||
-14,
|
||||
2.967,
|
||||
-14,
|
||||
3.367,
|
||||
-14,
|
||||
1,
|
||||
3.511,
|
||||
-14,
|
||||
3.656,
|
||||
-12,
|
||||
3.8,
|
||||
-12,
|
||||
0,
|
||||
4.433,
|
||||
-12
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamCheek",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
4.433,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Segments": [
|
||||
0,
|
||||
1.2,
|
||||
1,
|
||||
0.389,
|
||||
1.2,
|
||||
0.778,
|
||||
1.148,
|
||||
1.167,
|
||||
1,
|
||||
1,
|
||||
1.211,
|
||||
0.983,
|
||||
1.256,
|
||||
0,
|
||||
1.3,
|
||||
0,
|
||||
1,
|
||||
1.322,
|
||||
0,
|
||||
1.344,
|
||||
0,
|
||||
1.367,
|
||||
0,
|
||||
1,
|
||||
1.422,
|
||||
0,
|
||||
1.478,
|
||||
1,
|
||||
1.533,
|
||||
1,
|
||||
1,
|
||||
1.944,
|
||||
1,
|
||||
2.356,
|
||||
1,
|
||||
2.767,
|
||||
1,
|
||||
1,
|
||||
2.811,
|
||||
1,
|
||||
2.856,
|
||||
0,
|
||||
2.9,
|
||||
0,
|
||||
1,
|
||||
2.922,
|
||||
0,
|
||||
2.944,
|
||||
0,
|
||||
2.967,
|
||||
0,
|
||||
1,
|
||||
3.022,
|
||||
0,
|
||||
3.078,
|
||||
1,
|
||||
3.133,
|
||||
1,
|
||||
0,
|
||||
4.433,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeLSmile",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeROpen",
|
||||
"Segments": [
|
||||
0,
|
||||
1.2,
|
||||
1,
|
||||
0.389,
|
||||
1.2,
|
||||
0.778,
|
||||
1.148,
|
||||
1.167,
|
||||
1,
|
||||
1,
|
||||
1.211,
|
||||
0.983,
|
||||
1.256,
|
||||
0,
|
||||
1.3,
|
||||
0,
|
||||
1,
|
||||
1.322,
|
||||
0,
|
||||
1.344,
|
||||
0,
|
||||
1.367,
|
||||
0,
|
||||
1,
|
||||
1.422,
|
||||
0,
|
||||
1.478,
|
||||
1,
|
||||
1.533,
|
||||
1,
|
||||
1,
|
||||
1.944,
|
||||
1,
|
||||
2.356,
|
||||
1,
|
||||
2.767,
|
||||
1,
|
||||
1,
|
||||
2.811,
|
||||
1,
|
||||
2.856,
|
||||
0,
|
||||
2.9,
|
||||
0,
|
||||
1,
|
||||
2.922,
|
||||
0,
|
||||
2.944,
|
||||
0,
|
||||
2.967,
|
||||
0,
|
||||
1,
|
||||
3.022,
|
||||
0,
|
||||
3.078,
|
||||
1,
|
||||
3.133,
|
||||
1,
|
||||
0,
|
||||
4.433,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeRSmile",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeBallX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.211,
|
||||
0,
|
||||
0.422,
|
||||
0,
|
||||
0.633,
|
||||
0,
|
||||
1,
|
||||
0.911,
|
||||
0,
|
||||
1.189,
|
||||
-0.44,
|
||||
1.467,
|
||||
-0.44,
|
||||
1,
|
||||
1.689,
|
||||
-0.44,
|
||||
1.911,
|
||||
0.79,
|
||||
2.133,
|
||||
0.79,
|
||||
1,
|
||||
2.511,
|
||||
0.79,
|
||||
2.889,
|
||||
0,
|
||||
3.267,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeBallY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.211,
|
||||
0,
|
||||
0.422,
|
||||
0,
|
||||
0.633,
|
||||
0,
|
||||
1,
|
||||
0.911,
|
||||
0,
|
||||
1.189,
|
||||
-1,
|
||||
1.467,
|
||||
-1,
|
||||
1,
|
||||
1.689,
|
||||
-1,
|
||||
1.911,
|
||||
-1,
|
||||
2.133,
|
||||
-1,
|
||||
1,
|
||||
2.511,
|
||||
-1,
|
||||
2.889,
|
||||
0,
|
||||
3.267,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLY",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.544,
|
||||
1,
|
||||
1.089,
|
||||
1,
|
||||
1.633,
|
||||
1,
|
||||
1,
|
||||
1.856,
|
||||
1,
|
||||
2.078,
|
||||
0,
|
||||
2.3,
|
||||
0,
|
||||
1,
|
||||
2.5,
|
||||
0,
|
||||
2.7,
|
||||
1,
|
||||
2.9,
|
||||
1,
|
||||
0,
|
||||
4.433,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRY",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.544,
|
||||
1,
|
||||
1.089,
|
||||
1,
|
||||
1.633,
|
||||
1,
|
||||
1,
|
||||
1.856,
|
||||
1,
|
||||
2.078,
|
||||
0,
|
||||
2.3,
|
||||
0,
|
||||
1,
|
||||
2.5,
|
||||
0,
|
||||
2.7,
|
||||
1,
|
||||
2.9,
|
||||
1,
|
||||
0,
|
||||
4.433,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLAngle",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRAngle",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLForm",
|
||||
"Segments": [
|
||||
0,
|
||||
-1,
|
||||
0,
|
||||
4.433,
|
||||
-1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRForm",
|
||||
"Segments": [
|
||||
0,
|
||||
-1,
|
||||
0,
|
||||
4.433,
|
||||
-1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.244,
|
||||
0,
|
||||
0.489,
|
||||
0,
|
||||
0.733,
|
||||
0,
|
||||
1,
|
||||
0.933,
|
||||
0,
|
||||
1.133,
|
||||
-7,
|
||||
1.333,
|
||||
-7,
|
||||
1,
|
||||
1.644,
|
||||
-7,
|
||||
1.956,
|
||||
0,
|
||||
2.267,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.244,
|
||||
0,
|
||||
0.489,
|
||||
0,
|
||||
0.733,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleZ",
|
||||
"Segments": [
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
0.233,
|
||||
2,
|
||||
0.467,
|
||||
0,
|
||||
0.7,
|
||||
0,
|
||||
1,
|
||||
0.733,
|
||||
0,
|
||||
0.767,
|
||||
0,
|
||||
0.8,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1.2,
|
||||
-4,
|
||||
1.4,
|
||||
-4,
|
||||
1,
|
||||
1.711,
|
||||
-4,
|
||||
2.022,
|
||||
5,
|
||||
2.333,
|
||||
5,
|
||||
1,
|
||||
2.567,
|
||||
5,
|
||||
2.8,
|
||||
3.64,
|
||||
3.033,
|
||||
0,
|
||||
1,
|
||||
3.133,
|
||||
-1.56,
|
||||
3.233,
|
||||
-3,
|
||||
3.333,
|
||||
-3,
|
||||
1,
|
||||
3.467,
|
||||
-3,
|
||||
3.6,
|
||||
-2,
|
||||
3.733,
|
||||
-2,
|
||||
0,
|
||||
4.433,
|
||||
-2
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBreath",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.189,
|
||||
0,
|
||||
0.378,
|
||||
1,
|
||||
0.567,
|
||||
1,
|
||||
1,
|
||||
0.711,
|
||||
1,
|
||||
0.856,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1.222,
|
||||
0,
|
||||
1.444,
|
||||
1,
|
||||
1.667,
|
||||
1,
|
||||
1,
|
||||
1.889,
|
||||
1,
|
||||
2.111,
|
||||
0,
|
||||
2.333,
|
||||
0,
|
||||
1,
|
||||
2.544,
|
||||
0,
|
||||
2.756,
|
||||
1,
|
||||
2.967,
|
||||
1,
|
||||
1,
|
||||
3.167,
|
||||
1,
|
||||
3.367,
|
||||
0,
|
||||
3.567,
|
||||
0,
|
||||
0,
|
||||
4.433,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamShoulder",
|
||||
"Segments": [
|
||||
0,
|
||||
0.1,
|
||||
1,
|
||||
0.467,
|
||||
0.1,
|
||||
0.933,
|
||||
1,
|
||||
1.4,
|
||||
1,
|
||||
1,
|
||||
1.844,
|
||||
1,
|
||||
2.289,
|
||||
1,
|
||||
2.733,
|
||||
1,
|
||||
1,
|
||||
2.967,
|
||||
1,
|
||||
3.2,
|
||||
-1,
|
||||
3.433,
|
||||
-1,
|
||||
0,
|
||||
4.433,
|
||||
-1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamLeg",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
4.433,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmLA",
|
||||
"Segments": [
|
||||
0,
|
||||
-10,
|
||||
0,
|
||||
4.433,
|
||||
-10
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmRA",
|
||||
"Segments": [
|
||||
0,
|
||||
-10,
|
||||
0,
|
||||
4.433,
|
||||
-10
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamHairAhoge",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.3,
|
||||
0,
|
||||
0.6,
|
||||
0,
|
||||
0.9,
|
||||
-0.012,
|
||||
1,
|
||||
1.067,
|
||||
-0.019,
|
||||
1.233,
|
||||
-6.827,
|
||||
1.4,
|
||||
-6.827,
|
||||
1,
|
||||
1.511,
|
||||
-6.827,
|
||||
1.622,
|
||||
7.958,
|
||||
1.733,
|
||||
7.958,
|
||||
1,
|
||||
1.944,
|
||||
7.958,
|
||||
2.156,
|
||||
-7.565,
|
||||
2.367,
|
||||
-7.565,
|
||||
1,
|
||||
2.5,
|
||||
-7.565,
|
||||
2.633,
|
||||
9.434,
|
||||
2.767,
|
||||
9.434,
|
||||
1,
|
||||
2.978,
|
||||
9.434,
|
||||
3.189,
|
||||
-8.871,
|
||||
3.4,
|
||||
-8.871,
|
||||
1,
|
||||
3.5,
|
||||
-8.871,
|
||||
3.6,
|
||||
7.588,
|
||||
3.7,
|
||||
7.588,
|
||||
1,
|
||||
3.789,
|
||||
7.588,
|
||||
3.878,
|
||||
-3.904,
|
||||
3.967,
|
||||
-3.904,
|
||||
1,
|
||||
4.011,
|
||||
-3.904,
|
||||
4.056,
|
||||
-0.032,
|
||||
4.1,
|
||||
-0.032,
|
||||
0,
|
||||
4.433,
|
||||
-0.032
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "PartOpacity",
|
||||
"Id": "PartArmA",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
4.43,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "PartOpacity",
|
||||
"Id": "PartArmB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.43,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,927 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Meta": {
|
||||
"Duration": 1.9,
|
||||
"Fps": 30.0,
|
||||
"Loop": true,
|
||||
"AreBeziersRestricted": false,
|
||||
"CurveCount": 30,
|
||||
"TotalSegmentCount": 121,
|
||||
"TotalPointCount": 331,
|
||||
"UserDataCount": 0,
|
||||
"TotalUserDataSize": 0
|
||||
},
|
||||
"Curves": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleZ",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.444,
|
||||
0,
|
||||
0.556,
|
||||
8,
|
||||
0.667,
|
||||
8,
|
||||
0,
|
||||
1.9,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamCheek",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.111,
|
||||
1,
|
||||
0.222,
|
||||
1,
|
||||
0.333,
|
||||
1,
|
||||
1,
|
||||
0.378,
|
||||
1,
|
||||
0.422,
|
||||
0,
|
||||
0.467,
|
||||
0,
|
||||
1,
|
||||
0.522,
|
||||
0,
|
||||
0.578,
|
||||
1.2,
|
||||
0.633,
|
||||
1.2,
|
||||
1,
|
||||
0.744,
|
||||
1.2,
|
||||
0.856,
|
||||
1.2,
|
||||
0.967,
|
||||
1.2,
|
||||
1,
|
||||
0.989,
|
||||
1.2,
|
||||
1.011,
|
||||
0,
|
||||
1.033,
|
||||
0,
|
||||
1,
|
||||
1.067,
|
||||
0,
|
||||
1.1,
|
||||
1.2,
|
||||
1.133,
|
||||
1.2,
|
||||
1,
|
||||
1.167,
|
||||
1.2,
|
||||
1.2,
|
||||
1.2,
|
||||
1.233,
|
||||
1.2,
|
||||
1,
|
||||
1.267,
|
||||
1.2,
|
||||
1.3,
|
||||
0,
|
||||
1.333,
|
||||
0,
|
||||
1,
|
||||
1.356,
|
||||
0,
|
||||
1.378,
|
||||
1.2,
|
||||
1.4,
|
||||
1.2,
|
||||
0,
|
||||
1.9,
|
||||
1.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeLSmile",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeROpen",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.111,
|
||||
1,
|
||||
0.222,
|
||||
1,
|
||||
0.333,
|
||||
1,
|
||||
1,
|
||||
0.378,
|
||||
1,
|
||||
0.422,
|
||||
0,
|
||||
0.467,
|
||||
0,
|
||||
1,
|
||||
0.522,
|
||||
0,
|
||||
0.578,
|
||||
1.2,
|
||||
0.633,
|
||||
1.2,
|
||||
1,
|
||||
0.744,
|
||||
1.2,
|
||||
0.856,
|
||||
1.2,
|
||||
0.967,
|
||||
1.2,
|
||||
1,
|
||||
0.989,
|
||||
1.2,
|
||||
1.011,
|
||||
0,
|
||||
1.033,
|
||||
0,
|
||||
1,
|
||||
1.067,
|
||||
0,
|
||||
1.1,
|
||||
1.2,
|
||||
1.133,
|
||||
1.2,
|
||||
1,
|
||||
1.167,
|
||||
1.2,
|
||||
1.2,
|
||||
1.2,
|
||||
1.233,
|
||||
1.2,
|
||||
1,
|
||||
1.267,
|
||||
1.2,
|
||||
1.3,
|
||||
0,
|
||||
1.333,
|
||||
0,
|
||||
1,
|
||||
1.356,
|
||||
0,
|
||||
1.378,
|
||||
1.2,
|
||||
1.4,
|
||||
1.2,
|
||||
0,
|
||||
1.9,
|
||||
1.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeRSmile",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0,
|
||||
0.667,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeBallX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0,
|
||||
0.667,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeBallY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0,
|
||||
0.667,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0.26,
|
||||
0.667,
|
||||
0.26,
|
||||
0,
|
||||
1.9,
|
||||
0.26
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0.36,
|
||||
0.667,
|
||||
0.36,
|
||||
0,
|
||||
1.9,
|
||||
0.36
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0,
|
||||
0.667,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
-0.27,
|
||||
0.667,
|
||||
-0.27,
|
||||
0,
|
||||
1.9,
|
||||
-0.27
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLAngle",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0.26,
|
||||
0.667,
|
||||
0.26,
|
||||
0,
|
||||
1.9,
|
||||
0.26
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRAngle",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
-0.03,
|
||||
0.667,
|
||||
-0.03,
|
||||
0,
|
||||
1.9,
|
||||
-0.03
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLForm",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0.33,
|
||||
0.667,
|
||||
0.33,
|
||||
0,
|
||||
1.9,
|
||||
0.33
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRForm",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.356,
|
||||
0,
|
||||
0.378,
|
||||
0,
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
0.489,
|
||||
0,
|
||||
0.578,
|
||||
0.21,
|
||||
0.667,
|
||||
0.21,
|
||||
0,
|
||||
1.9,
|
||||
0.21
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.444,
|
||||
0,
|
||||
0.556,
|
||||
-6,
|
||||
0.667,
|
||||
-6,
|
||||
0,
|
||||
1.9,
|
||||
-6
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.422,
|
||||
0,
|
||||
0.511,
|
||||
10,
|
||||
0.6,
|
||||
10,
|
||||
1,
|
||||
0.667,
|
||||
10,
|
||||
0.733,
|
||||
-6,
|
||||
0.8,
|
||||
-6,
|
||||
1,
|
||||
0.833,
|
||||
-6,
|
||||
0.867,
|
||||
5,
|
||||
0.9,
|
||||
5,
|
||||
1,
|
||||
1.011,
|
||||
5,
|
||||
1.122,
|
||||
0,
|
||||
1.233,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleZ",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.444,
|
||||
0,
|
||||
0.556,
|
||||
-3,
|
||||
0.667,
|
||||
-3,
|
||||
0,
|
||||
1.9,
|
||||
-3
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBreath",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamShoulder",
|
||||
"Segments": [
|
||||
0,
|
||||
-0.062,
|
||||
1,
|
||||
0.111,
|
||||
-0.062,
|
||||
0.222,
|
||||
-0.103,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.589,
|
||||
0.238,
|
||||
0.844,
|
||||
1,
|
||||
1.1,
|
||||
1,
|
||||
0,
|
||||
1.9,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmLA",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.478,
|
||||
0,
|
||||
0.622,
|
||||
-10,
|
||||
0.767,
|
||||
-10,
|
||||
1,
|
||||
0.811,
|
||||
-10,
|
||||
0.856,
|
||||
-8.2,
|
||||
0.9,
|
||||
-8.2,
|
||||
0,
|
||||
1.9,
|
||||
-8.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmRA",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
1,
|
||||
0.478,
|
||||
0,
|
||||
0.622,
|
||||
-10,
|
||||
0.767,
|
||||
-10,
|
||||
1,
|
||||
0.811,
|
||||
-10,
|
||||
0.856,
|
||||
-7.2,
|
||||
0.9,
|
||||
-7.2,
|
||||
0,
|
||||
1.9,
|
||||
-7.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmLB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmRB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
0,
|
||||
0.333,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamHairAhoge",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.111,
|
||||
0,
|
||||
0.222,
|
||||
1.9,
|
||||
0.333,
|
||||
5.2,
|
||||
1,
|
||||
0.444,
|
||||
8.5,
|
||||
0.556,
|
||||
9.926,
|
||||
0.667,
|
||||
9.926,
|
||||
1,
|
||||
0.744,
|
||||
9.926,
|
||||
0.822,
|
||||
-10,
|
||||
0.9,
|
||||
-10,
|
||||
1,
|
||||
0.956,
|
||||
-10,
|
||||
1.011,
|
||||
6,
|
||||
1.067,
|
||||
6,
|
||||
1,
|
||||
1.144,
|
||||
6,
|
||||
1.222,
|
||||
-4,
|
||||
1.3,
|
||||
-4,
|
||||
1,
|
||||
1.367,
|
||||
-4,
|
||||
1.433,
|
||||
0,
|
||||
1.5,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "PartOpacity",
|
||||
"Id": "PartArmA",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1.9,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "PartOpacity",
|
||||
"Id": "PartArmB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1.9,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,924 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Meta": {
|
||||
"Duration": 4.17,
|
||||
"Fps": 30.0,
|
||||
"Loop": true,
|
||||
"AreBeziersRestricted": false,
|
||||
"CurveCount": 31,
|
||||
"TotalSegmentCount": 118,
|
||||
"TotalPointCount": 321,
|
||||
"UserDataCount": 0,
|
||||
"TotalUserDataSize": 0
|
||||
},
|
||||
"Curves": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.4,
|
||||
0,
|
||||
0.6,
|
||||
0,
|
||||
0.8,
|
||||
0,
|
||||
1,
|
||||
1.067,
|
||||
0,
|
||||
1.333,
|
||||
1.041,
|
||||
1.6,
|
||||
1.041,
|
||||
1,
|
||||
1.844,
|
||||
1.041,
|
||||
2.089,
|
||||
-8,
|
||||
2.333,
|
||||
-8,
|
||||
1,
|
||||
2.656,
|
||||
-8,
|
||||
2.978,
|
||||
6,
|
||||
3.3,
|
||||
6,
|
||||
0,
|
||||
4.167,
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.344,
|
||||
0,
|
||||
0.489,
|
||||
-30,
|
||||
0.633,
|
||||
-30,
|
||||
0,
|
||||
4.167,
|
||||
-30
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamAngleZ",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamCheek",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.067,
|
||||
1,
|
||||
0.133,
|
||||
1,
|
||||
0.2,
|
||||
1,
|
||||
1,
|
||||
0.311,
|
||||
1,
|
||||
0.422,
|
||||
0.988,
|
||||
0.533,
|
||||
0.8,
|
||||
1,
|
||||
0.589,
|
||||
0.706,
|
||||
0.644,
|
||||
0,
|
||||
0.7,
|
||||
0,
|
||||
1,
|
||||
0.722,
|
||||
0,
|
||||
0.744,
|
||||
0,
|
||||
0.767,
|
||||
0,
|
||||
1,
|
||||
0.822,
|
||||
0,
|
||||
0.878,
|
||||
0.8,
|
||||
0.933,
|
||||
0.8,
|
||||
1,
|
||||
1.422,
|
||||
0.8,
|
||||
1.911,
|
||||
0.8,
|
||||
2.4,
|
||||
0.8,
|
||||
1,
|
||||
2.456,
|
||||
0.8,
|
||||
2.511,
|
||||
0,
|
||||
2.567,
|
||||
0,
|
||||
1,
|
||||
2.589,
|
||||
0,
|
||||
2.611,
|
||||
0,
|
||||
2.633,
|
||||
0,
|
||||
1,
|
||||
2.689,
|
||||
0,
|
||||
2.744,
|
||||
0.8,
|
||||
2.8,
|
||||
0.8,
|
||||
0,
|
||||
4.167,
|
||||
0.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeLSmile",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeROpen",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.067,
|
||||
1,
|
||||
0.133,
|
||||
1,
|
||||
0.2,
|
||||
1,
|
||||
1,
|
||||
0.311,
|
||||
1,
|
||||
0.422,
|
||||
0.988,
|
||||
0.533,
|
||||
0.8,
|
||||
1,
|
||||
0.589,
|
||||
0.706,
|
||||
0.644,
|
||||
0,
|
||||
0.7,
|
||||
0,
|
||||
1,
|
||||
0.722,
|
||||
0,
|
||||
0.744,
|
||||
0,
|
||||
0.767,
|
||||
0,
|
||||
1,
|
||||
0.822,
|
||||
0,
|
||||
0.878,
|
||||
0.8,
|
||||
0.933,
|
||||
0.8,
|
||||
1,
|
||||
1.422,
|
||||
0.8,
|
||||
1.911,
|
||||
0.8,
|
||||
2.4,
|
||||
0.8,
|
||||
1,
|
||||
2.456,
|
||||
0.8,
|
||||
2.511,
|
||||
0,
|
||||
2.567,
|
||||
0,
|
||||
1,
|
||||
2.589,
|
||||
0,
|
||||
2.611,
|
||||
0,
|
||||
2.633,
|
||||
0,
|
||||
1,
|
||||
2.689,
|
||||
0,
|
||||
2.744,
|
||||
0.8,
|
||||
2.8,
|
||||
0.8,
|
||||
0,
|
||||
4.167,
|
||||
0.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeRSmile",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
0,
|
||||
0.433,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeBallX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
0,
|
||||
0.433,
|
||||
0,
|
||||
1,
|
||||
0.667,
|
||||
0,
|
||||
0.9,
|
||||
0.004,
|
||||
1.133,
|
||||
-0.01,
|
||||
1,
|
||||
1.4,
|
||||
-0.025,
|
||||
1.667,
|
||||
-0.43,
|
||||
1.933,
|
||||
-0.43,
|
||||
1,
|
||||
2.211,
|
||||
-0.43,
|
||||
2.489,
|
||||
0.283,
|
||||
2.767,
|
||||
0.283,
|
||||
0,
|
||||
4.167,
|
||||
0.283
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamEyeBallY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
-1,
|
||||
0.433,
|
||||
-1,
|
||||
0,
|
||||
4.167,
|
||||
-1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
0.19,
|
||||
0.433,
|
||||
0.19,
|
||||
0,
|
||||
4.167,
|
||||
0.19
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
0.11,
|
||||
0.433,
|
||||
0.11,
|
||||
0,
|
||||
4.167,
|
||||
0.11
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
-0.48,
|
||||
0.433,
|
||||
-0.48,
|
||||
0,
|
||||
4.167,
|
||||
-0.48
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
-0.29,
|
||||
0.433,
|
||||
-0.29,
|
||||
0,
|
||||
4.167,
|
||||
-0.29
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLAngle",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
1,
|
||||
0.433,
|
||||
1,
|
||||
0,
|
||||
4.167,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRAngle",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
0.85,
|
||||
0.433,
|
||||
0.85,
|
||||
0,
|
||||
4.167,
|
||||
0.85
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowLForm",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
-0.75,
|
||||
0.433,
|
||||
-0.75,
|
||||
0,
|
||||
4.167,
|
||||
-0.75
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBrowRForm",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.278,
|
||||
0,
|
||||
0.356,
|
||||
-0.87,
|
||||
0.433,
|
||||
-0.87,
|
||||
0,
|
||||
4.167,
|
||||
-0.87
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleX",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.444,
|
||||
0,
|
||||
0.689,
|
||||
0,
|
||||
0.933,
|
||||
0,
|
||||
1,
|
||||
1.211,
|
||||
0,
|
||||
1.489,
|
||||
0,
|
||||
1.767,
|
||||
0,
|
||||
1,
|
||||
2.056,
|
||||
0,
|
||||
2.344,
|
||||
-6,
|
||||
2.633,
|
||||
-6,
|
||||
1,
|
||||
3.033,
|
||||
-6,
|
||||
3.433,
|
||||
10,
|
||||
3.833,
|
||||
10,
|
||||
0,
|
||||
4.167,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleY",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBodyAngleZ",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.8,
|
||||
0,
|
||||
1.4,
|
||||
-2,
|
||||
2,
|
||||
-2,
|
||||
1,
|
||||
2.456,
|
||||
-2,
|
||||
2.911,
|
||||
8.125,
|
||||
3.367,
|
||||
8.125,
|
||||
0,
|
||||
4.167,
|
||||
8.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamBreath",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamShoulder",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamLeg",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0.667,
|
||||
1,
|
||||
1.333,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2.267,
|
||||
1,
|
||||
2.533,
|
||||
0.948,
|
||||
2.8,
|
||||
0.948,
|
||||
0,
|
||||
4.167,
|
||||
0.948
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmLA",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.233,
|
||||
0,
|
||||
0.267,
|
||||
0,
|
||||
0.3,
|
||||
0,
|
||||
1,
|
||||
0.478,
|
||||
0,
|
||||
0.656,
|
||||
-10,
|
||||
0.833,
|
||||
-10,
|
||||
1,
|
||||
0.922,
|
||||
-10,
|
||||
1.011,
|
||||
-8.846,
|
||||
1.1,
|
||||
-8.846,
|
||||
1,
|
||||
1.467,
|
||||
-8.846,
|
||||
1.833,
|
||||
-8.835,
|
||||
2.2,
|
||||
-9.1,
|
||||
1,
|
||||
2.622,
|
||||
-9.405,
|
||||
3.044,
|
||||
-10,
|
||||
3.467,
|
||||
-10,
|
||||
0,
|
||||
4.167,
|
||||
-10
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmRA",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.233,
|
||||
0,
|
||||
0.267,
|
||||
0,
|
||||
0.3,
|
||||
0,
|
||||
1,
|
||||
0.478,
|
||||
0,
|
||||
0.656,
|
||||
-10,
|
||||
0.833,
|
||||
-10,
|
||||
1,
|
||||
0.922,
|
||||
-10,
|
||||
1.011,
|
||||
-8.972,
|
||||
1.1,
|
||||
-8.846,
|
||||
1,
|
||||
1.467,
|
||||
-8.328,
|
||||
1.833,
|
||||
-8.2,
|
||||
2.2,
|
||||
-8.2,
|
||||
1,
|
||||
2.622,
|
||||
-8.2,
|
||||
3.044,
|
||||
-10,
|
||||
3.467,
|
||||
-10,
|
||||
0,
|
||||
4.167,
|
||||
-10
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmLB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamArmRB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.167,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "ParamHairAhoge",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.067,
|
||||
0,
|
||||
0.133,
|
||||
0,
|
||||
0.2,
|
||||
0,
|
||||
1,
|
||||
0.233,
|
||||
0,
|
||||
0.267,
|
||||
-5,
|
||||
0.3,
|
||||
-5,
|
||||
1,
|
||||
0.378,
|
||||
-5,
|
||||
0.456,
|
||||
10,
|
||||
0.533,
|
||||
10,
|
||||
1,
|
||||
0.633,
|
||||
10,
|
||||
0.733,
|
||||
4,
|
||||
0.833,
|
||||
4,
|
||||
0,
|
||||
4.167,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "PartOpacity",
|
||||
"Id": "PartArmA",
|
||||
"Segments": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
4.17,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "PartOpacity",
|
||||
"Id": "PartArmB",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4.17,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 321 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
@@ -1,7 +1,7 @@
|
||||
// 主应用入口
|
||||
import { log } from './utils/logger.js';
|
||||
import { checkOpusLoaded, initOpusEncoder } from './core/audio/opus-codec.js';
|
||||
import { getUIController } from './ui/controller.js';
|
||||
import { uiController } from './ui/controller.js';
|
||||
import { getAudioPlayer } from './core/audio/player.js';
|
||||
import { initMcpTools } from './core/mcp/tools.js';
|
||||
|
||||
@@ -10,6 +10,7 @@ class App {
|
||||
constructor() {
|
||||
this.uiController = null;
|
||||
this.audioPlayer = null;
|
||||
this.live2dManager = null;
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
@@ -17,7 +18,7 @@ class App {
|
||||
log('正在初始化应用...', 'info');
|
||||
|
||||
// 初始化UI控制器
|
||||
this.uiController = getUIController();
|
||||
this.uiController = uiController;
|
||||
this.uiController.init();
|
||||
|
||||
// 检查Opus库
|
||||
@@ -33,13 +34,50 @@ class App {
|
||||
// 初始化MCP工具
|
||||
initMcpTools();
|
||||
|
||||
// 初始化Live2D
|
||||
await this.initLive2D();
|
||||
|
||||
log('应用初始化完成', 'success');
|
||||
}
|
||||
|
||||
// 初始化Live2D
|
||||
async initLive2D() {
|
||||
try {
|
||||
// 检查Live2DManager是否已加载
|
||||
if (typeof window.Live2DManager === 'undefined') {
|
||||
throw new Error('Live2DManager未加载,请检查脚本引入顺序');
|
||||
}
|
||||
|
||||
this.live2dManager = new window.Live2DManager();
|
||||
await this.live2dManager.initializeLive2D();
|
||||
|
||||
// 更新UI状态
|
||||
const live2dStatus = document.getElementById('live2dStatus');
|
||||
if (live2dStatus) {
|
||||
live2dStatus.textContent = '● 已加载';
|
||||
live2dStatus.className = 'status loaded';
|
||||
}
|
||||
|
||||
log('Live2D初始化完成', 'success');
|
||||
} catch (error) {
|
||||
log(`Live2D初始化失败: ${error.message}`, 'error');
|
||||
|
||||
// 更新UI状态
|
||||
const live2dStatus = document.getElementById('live2dStatus');
|
||||
if (live2dStatus) {
|
||||
live2dStatus.textContent = '● 加载失败';
|
||||
live2dStatus.className = 'status error';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并启动应用
|
||||
const app = new App();
|
||||
|
||||
// 将应用实例暴露到全局,供其他模块访问
|
||||
window.chatApp = app;
|
||||
|
||||
// DOM加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => app.init());
|
||||
|
||||
@@ -326,6 +326,11 @@ export class AudioRecorder {
|
||||
this.startVisualization(dataArray);
|
||||
}
|
||||
|
||||
// 立即通知录音开始,更新按钮状态
|
||||
if (this.onRecordingStart) {
|
||||
this.onRecordingStart(0);
|
||||
}
|
||||
|
||||
// 启动录音计时器
|
||||
let recordingSeconds = 0;
|
||||
this.recordingTimer = setInterval(() => {
|
||||
|
||||
@@ -23,6 +23,10 @@ export class StreamingContext {
|
||||
this.totalSamples = 0; // 累积的总样本数
|
||||
this.lastPlayTime = 0; // 上次播放的时间戳
|
||||
this.scheduledEndTime = 0; // 已调度音频的结束时间
|
||||
|
||||
// 初始化分析器节点(供Live2D使用)
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
}
|
||||
|
||||
// 缓存音频数组
|
||||
@@ -108,6 +112,11 @@ export class StreamingContext {
|
||||
log('音频缓冲已清空', 'success');
|
||||
}
|
||||
|
||||
// 获取分析器节点(供Live2D使用)
|
||||
getAnalyser() {
|
||||
return this.analyser;
|
||||
}
|
||||
|
||||
// 将Opus数据解码为PCM
|
||||
async decodeOpusFrames() {
|
||||
if (!this.opusDecoder) {
|
||||
@@ -182,7 +191,8 @@ export class StreamingContext {
|
||||
const currentTime = this.audioContext.currentTime;
|
||||
const startTime = Math.max(this.scheduledEndTime, currentTime);
|
||||
|
||||
// 直接连接到输出
|
||||
// 连接到分析器和输出
|
||||
this.source.connect(this.analyser);
|
||||
this.source.connect(this.audioContext.destination);
|
||||
|
||||
log(`调度播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / this.sampleRate).toFixed(2)} 秒`, 'debug');
|
||||
|
||||
@@ -48,7 +48,9 @@ function renderMcpTools() {
|
||||
const container = document.getElementById('mcpToolsContainer');
|
||||
const countSpan = document.getElementById('mcpToolsCount');
|
||||
|
||||
countSpan.textContent = `${mcpTools.length} 个工具`;
|
||||
if (countSpan) {
|
||||
countSpan.textContent = `${mcpTools.length} 个工具`;
|
||||
}
|
||||
|
||||
if (mcpTools.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999;">暂无工具,点击下方按钮添加新工具</div>';
|
||||
@@ -65,12 +67,10 @@ function renderMcpTools() {
|
||||
<div class="mcp-tool-header">
|
||||
<div class="mcp-tool-name">${tool.name}</div>
|
||||
<div class="mcp-tool-actions">
|
||||
<button onclick="window.mcpModule.editMcpTool(${index})"
|
||||
style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #2196f3; color: white; cursor: pointer; font-size: 12px;">
|
||||
<button class="mcp-edit-btn" onclick="window.mcpModule.editMcpTool(${index})">
|
||||
✏️ 编辑
|
||||
</button>
|
||||
<button onclick="window.mcpModule.deleteMcpTool(${index})"
|
||||
style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #f44336; color: white; cursor: pointer; font-size: 12px;">
|
||||
<button class="mcp-delete-btn" onclick="window.mcpModule.deleteMcpTool(${index})">
|
||||
🗑️ 删除
|
||||
</button>
|
||||
</div>
|
||||
@@ -218,6 +218,9 @@ function setupMcpEventListeners() {
|
||||
panel.classList.toggle('expanded');
|
||||
toggleBtn.textContent = isExpanded ? '展开' : '收起';
|
||||
});
|
||||
|
||||
// 确保面板默认展开
|
||||
panel.classList.add('expanded');
|
||||
|
||||
addBtn.addEventListener('click', () => openMcpModal());
|
||||
closeBtn.addEventListener('click', closeMcpModal);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// WebSocket消息处理模块
|
||||
import { log } from '../../utils/logger.js';
|
||||
import { addMessage } from '../../ui/dom-helper.js';
|
||||
import { webSocketConnect } from './ota-connector.js';
|
||||
import { getConfig, saveConnectionUrls } from '../../config/manager.js';
|
||||
import { getAudioPlayer } from '../audio/player.js';
|
||||
@@ -15,6 +14,7 @@ export class WebSocketHandler {
|
||||
this.onRecordButtonStateChange = null;
|
||||
this.onSessionStateChange = null;
|
||||
this.onSessionEmotionChange = null;
|
||||
this.onChatMessage = null; // 新增:聊天消息回调
|
||||
this.currentSessionId = null;
|
||||
this.isRemoteSpeaking = false;
|
||||
}
|
||||
@@ -79,30 +79,45 @@ export class WebSocketHandler {
|
||||
log(`收到音频控制消息: ${JSON.stringify(message)}`, 'info');
|
||||
} else if (message.type === 'stt') {
|
||||
log(`识别结果: ${message.text}`, 'info');
|
||||
addMessage(`${message.text}`, true);
|
||||
// 使用新的聊天消息回调显示STT消息
|
||||
if (this.onChatMessage && message.text) {
|
||||
this.onChatMessage(message.text, true);
|
||||
}
|
||||
} else if (message.type === 'llm') {
|
||||
log(`大模型回复: ${message.text}`, 'info');
|
||||
// 使用新的聊天消息回调显示LLM回复
|
||||
if (this.onChatMessage && message.text) {
|
||||
this.onChatMessage(message.text, false);
|
||||
}
|
||||
|
||||
// 如果包含表情,更新sessionStatus表情
|
||||
// 如果包含表情,更新sessionStatus表情并触发Live2D动作
|
||||
if (message.text && /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(message.text)) {
|
||||
// 提取表情符号
|
||||
const emojiMatch = message.text.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u);
|
||||
if (emojiMatch && this.onSessionEmotionChange) {
|
||||
this.onSessionEmotionChange(emojiMatch[0]);
|
||||
}
|
||||
|
||||
// 触发Live2D情绪动作
|
||||
if (message.emotion) {
|
||||
console.log(`收到情绪消息: emotion=${message.emotion}, text=${message.text}`);
|
||||
this.triggerLive2DEmotionAction(message.emotion);
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当文本不仅仅是表情时,才添加到对话中
|
||||
// 移除文本中的表情后检查是否还有内容
|
||||
const textWithoutEmoji = message.text ? message.text.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim() : '';
|
||||
if (textWithoutEmoji) {
|
||||
addMessage(message.text);
|
||||
if (textWithoutEmoji && this.onChatMessage) {
|
||||
this.onChatMessage(message.text, false);
|
||||
}
|
||||
} else if (message.type === 'mcp') {
|
||||
this.handleMCPMessage(message);
|
||||
} else {
|
||||
log(`未知消息类型: ${message.type}`, 'info');
|
||||
addMessage(JSON.stringify(message, null, 2));
|
||||
if (this.onChatMessage) {
|
||||
this.onChatMessage(`未知消息类型: ${message.type}\n${JSON.stringify(message, null, 2)}`, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,13 +130,26 @@ export class WebSocketHandler {
|
||||
if (this.onSessionStateChange) {
|
||||
this.onSessionStateChange(true);
|
||||
}
|
||||
|
||||
// 启动Live2D说话动画
|
||||
this.startLive2DTalking();
|
||||
} else if (message.state === 'sentence_start') {
|
||||
log(`服务器发送语音段: ${message.text}`, 'info');
|
||||
if (message.text) {
|
||||
addMessage(message.text);
|
||||
this.ttsSentenceCount = (this.ttsSentenceCount || 0) + 1;
|
||||
|
||||
if (message.text && this.onChatMessage) {
|
||||
this.onChatMessage(message.text, false);
|
||||
}
|
||||
|
||||
// 确保动画在句子开始时运行
|
||||
const live2dManager = window.chatApp?.live2dManager;
|
||||
if (live2dManager && !live2dManager.isTalking) {
|
||||
this.startLive2DTalking();
|
||||
}
|
||||
} else if (message.state === 'sentence_end') {
|
||||
log(`语音段结束: ${message.text}`, 'info');
|
||||
|
||||
// 句子结束时不清除动画,等待下一个句子或最终停止
|
||||
} else if (message.state === 'stop') {
|
||||
log('服务器语音传输结束,清空所有音频缓冲', 'info');
|
||||
|
||||
@@ -136,6 +164,57 @@ export class WebSocketHandler {
|
||||
if (this.onSessionStateChange) {
|
||||
this.onSessionStateChange(false);
|
||||
}
|
||||
|
||||
// 延迟停止Live2D说话动画,确保所有句子都播放完毕
|
||||
setTimeout(() => {
|
||||
this.stopLive2DTalking();
|
||||
this.ttsSentenceCount = 0; // 重置计数器
|
||||
}, 1000); // 1秒延迟,确保所有句子都完成
|
||||
}
|
||||
}
|
||||
|
||||
// 启动Live2D说话动画
|
||||
startLive2DTalking() {
|
||||
try {
|
||||
// 获取Live2D管理器实例
|
||||
const live2dManager = window.chatApp?.live2dManager;
|
||||
if (live2dManager && live2dManager.live2dModel) {
|
||||
// 使用音频播放器的分析器节点
|
||||
live2dManager.startTalking();
|
||||
log('Live2D说话动画已启动', 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`启动Live2D说话动画失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止Live2D说话动画
|
||||
stopLive2DTalking() {
|
||||
try {
|
||||
const live2dManager = window.chatApp?.live2dManager;
|
||||
if (live2dManager) {
|
||||
live2dManager.stopTalking();
|
||||
log('Live2D说话动画已停止', 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`停止Live2D说话动画失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Live2D音频分析器
|
||||
initializeLive2DAudioAnalyzer() {
|
||||
try {
|
||||
const live2dManager = window.chatApp?.live2dManager;
|
||||
if (live2dManager) {
|
||||
// 初始化音频分析器(使用音频播放器的上下文)
|
||||
if (live2dManager.initializeAudioAnalyzer()) {
|
||||
log('Live2D音频分析器初始化完成,已连接到音频播放器', 'success');
|
||||
} else {
|
||||
log('Live2D音频分析器初始化失败,将使用模拟动画', 'warning');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`初始化Live2D音频分析器失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +351,9 @@ export class WebSocketHandler {
|
||||
this.onSessionStateChange(false);
|
||||
}
|
||||
|
||||
// 在WebSocket连接成功时初始化Live2D音频分析器
|
||||
this.initializeLive2DAudioAnalyzer();
|
||||
|
||||
await this.sendHelloMessage();
|
||||
};
|
||||
|
||||
@@ -304,9 +386,8 @@ export class WebSocketHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
log(`WebSocket消息处理错误: ${error.message}`, 'error');
|
||||
if (typeof event.data === 'string') {
|
||||
addMessage(event.data);
|
||||
}
|
||||
// 不再使用旧的addMessage函数,因为conversationDiv元素不存在
|
||||
// 错误消息将通过其他方式显示
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -355,6 +436,24 @@ export class WebSocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发Live2D情绪动作
|
||||
* @param {string} emotion - 情绪名称
|
||||
*/
|
||||
triggerLive2DEmotionAction(emotion) {
|
||||
try {
|
||||
const live2dManager = window.chatApp?.live2dManager;
|
||||
if (live2dManager && typeof live2dManager.triggerEmotionAction === 'function') {
|
||||
live2dManager.triggerEmotionAction(emotion);
|
||||
log(`触发Live2D情绪动作: ${emotion}`, 'info');
|
||||
} else {
|
||||
log(`无法触发Live2D情绪动作: Live2D管理器未找到或方法不可用`, 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`触发Live2D情绪动作失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取WebSocket实例
|
||||
getWebSocket() {
|
||||
return this.websocket;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* Live2D 管理器
|
||||
* 负责 Live2D 模型的初始化、嘴部动画控制等功能
|
||||
*/
|
||||
class Live2DManager {
|
||||
constructor() {
|
||||
this.live2dApp = null;
|
||||
this.live2dModel = null;
|
||||
this.isTalking = false;
|
||||
this.mouthAnimationId = null;
|
||||
this.mouthParam = 'ParamMouthOpenY';
|
||||
this.audioContext = null;
|
||||
this.analyser = null;
|
||||
this.dataArray = null;
|
||||
this.lastEmotionActionTime = null; // 上次情绪触发动作的时间
|
||||
|
||||
// 情绪到动作的映射
|
||||
this.emotionToActionMap = {
|
||||
'happy': 'FlickUp', // 开心-向上轻扫动作
|
||||
'laughing': 'FlickUp', // 大笑-向上轻扫动作
|
||||
'funny': 'FlickUp', // 搞笑-向上轻扫动作
|
||||
'sad': 'FlickDown', // 伤心-向下轻扫动作
|
||||
'crying': 'FlickDown', // 哭泣-向下轻扫动作
|
||||
'angry': 'Tap@Body', // 生气-身体点击动作
|
||||
'surprised': 'Tap', // 惊讶-点击动作
|
||||
'neutral': 'Flick', // 平常-轻扫动作
|
||||
'default': 'Flick@Body' // 默认-身体轻扫动作
|
||||
};
|
||||
|
||||
// 单/双击判定配置与状态
|
||||
this._lastClickTime = 0;
|
||||
this._lastClickPos = { x: 0, y: 0 };
|
||||
this._singleClickTimer = null;
|
||||
this._doubleClickMs = 280; // 双击时间阈值(ms)
|
||||
this._doubleClickDist = 16; // 双击允许的最大位移(px)
|
||||
// 滑动判定
|
||||
this._pointerDown = false;
|
||||
this._downPos = { x: 0, y: 0 };
|
||||
this._downTime = 0;
|
||||
this._downArea = 'Body';
|
||||
this._movedBeyondClick = false;
|
||||
this._swipeMinDist = 24; // 触发滑动的最小距离
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Live2D
|
||||
*/
|
||||
async initializeLive2D() {
|
||||
try {
|
||||
const canvas = document.getElementById('live2d-stage');
|
||||
|
||||
// 供内部使用
|
||||
window.PIXI = PIXI;
|
||||
|
||||
this.live2dApp = new PIXI.Application({
|
||||
view: canvas,
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth,
|
||||
resolution: window.devicePixelRatio,
|
||||
autoDensity: true,
|
||||
antialias: true,
|
||||
backgroundAlpha: 0,
|
||||
});
|
||||
|
||||
// 加载 Live2D 模型 - 使用正确的相对路径
|
||||
this.live2dModel = await PIXI.live2d.Live2DModel.from('../hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json');
|
||||
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.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');
|
||||
}
|
||||
} else if (area === 'Head' || area === 'Face') {
|
||||
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 = '';
|
||||
// 经验阈值:模型可见矩形的上部 20% 视为"头部"区域
|
||||
if (relX >= 0.4 && relX <= 0.6) {
|
||||
if (relY <= 0.15) {
|
||||
area = 'Head';
|
||||
} else if (relY <= 0.23) {
|
||||
area = 'Face';
|
||||
} else {
|
||||
area = 'Body';
|
||||
}
|
||||
}
|
||||
if (area === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录按下状态用于滑动判定
|
||||
this._pointerDown = true;
|
||||
this._downPos = { x: global.x, y: global.y };
|
||||
this._downTime = performance.now();
|
||||
this._downArea = area;
|
||||
this._movedBeyondClick = false;
|
||||
|
||||
const now = performance.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;
|
||||
}
|
||||
if (typeof this.live2dModel.emit === 'function') {
|
||||
this.live2dModel.emit('doublehit', [area]);
|
||||
}
|
||||
this._lastClickTime = 0;
|
||||
this._pointerDown = false; // 双击完成,重置状态
|
||||
return;
|
||||
}
|
||||
|
||||
// 可能是单击:记录并延迟确认
|
||||
this._lastClickTime = now;
|
||||
this._lastClickPos = { x: global.x, y: global.y };
|
||||
if (this._singleClickTimer) {
|
||||
clearTimeout(this._singleClickTimer);
|
||||
this._singleClickTimer = null;
|
||||
}
|
||||
this._singleClickTimer = setTimeout(() => {
|
||||
// 若在等待期间发生了移动超过阈值,则不再当作单击
|
||||
if (!this._movedBeyondClick && typeof this.live2dModel.emit === 'function') {
|
||||
this.live2dModel.emit('singlehit', [area]);
|
||||
}
|
||||
this._singleClickTimer = null;
|
||||
this._lastClickTime = 0;
|
||||
}, this._doubleClickMs);
|
||||
} catch (e) {
|
||||
// 忽略自定义命中判断中的异常,避免影响主流程
|
||||
}
|
||||
});
|
||||
|
||||
// 指针移动:用于判定是否从"点击"升级为"滑动"
|
||||
this.live2dModel.on('pointermove', (event) => {
|
||||
try {
|
||||
if (!this._pointerDown) return;
|
||||
const global = event.data.global;
|
||||
const dx = global.x - this._downPos.x;
|
||||
const dy = global.y - this._downPos.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
// 使用 _doubleClickDist 作为点击/滑动的判定阈值
|
||||
if (dist > this._doubleClickDist) {
|
||||
this._movedBeyondClick = true;
|
||||
// 若已超出点击阈值,取消可能的单击触发
|
||||
if (this._singleClickTimer) {
|
||||
clearTimeout(this._singleClickTimer);
|
||||
this._singleClickTimer = null;
|
||||
}
|
||||
this._lastClickTime = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略移动判定中的异常
|
||||
}
|
||||
});
|
||||
|
||||
// 指针抬起:确认是否为滑动
|
||||
const handlePointerUp = (event) => {
|
||||
try {
|
||||
if (!this._pointerDown) return;
|
||||
const global = (event && event.data && event.data.global) ? event.data.global : { x: this._downPos.x, y: this._downPos.y };
|
||||
const dx = global.x - this._downPos.x;
|
||||
const dy = global.y - this._downPos.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
// 滑动:超过滑动最小距离则触发 swipe 事件(携带方向与区域)
|
||||
if (this._movedBeyondClick && dist >= this._swipeMinDist) {
|
||||
if (typeof this.live2dModel.emit === 'function') {
|
||||
const dir = Math.abs(dx) >= Math.abs(dy)
|
||||
? (dx > 0 ? 'right' : 'left')
|
||||
: (dy > 0 ? 'down' : 'up');
|
||||
this.live2dModel.emit('swipe', [this._downArea, dir]);
|
||||
}
|
||||
// 终止:不再让单击/双击触发
|
||||
if (this._singleClickTimer) {
|
||||
clearTimeout(this._singleClickTimer);
|
||||
this._singleClickTimer = null;
|
||||
}
|
||||
this._lastClickTime = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略抬起判定中的异常
|
||||
}
|
||||
finally {
|
||||
this._pointerDown = false;
|
||||
this._movedBeyondClick = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.live2dModel.on('pointerup', handlePointerUp);
|
||||
this.live2dModel.on('pointerupoutside', handlePointerUp);
|
||||
|
||||
// 添加窗口大小变化监听器,保持模型在Canvas中间和底部
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.live2dModel) {
|
||||
// 使用窗口实际尺寸重新计算模型位置
|
||||
this.live2dModel.x = (window.innerWidth - this.live2dModel.width) * 0.5;
|
||||
this.live2dModel.y = -50;
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('加载 Live2D 模型失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化音频分析器 - 使用音频播放器的分析器节点
|
||||
*/
|
||||
initializeAudioAnalyzer() {
|
||||
try {
|
||||
// 获取音频播放器实例
|
||||
const audioPlayer = window.chatApp?.audioPlayer;
|
||||
if (!audioPlayer) {
|
||||
console.warn('音频播放器未初始化,无法获取分析器节点');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取音频播放器的音频上下文
|
||||
this.audioContext = audioPlayer.getAudioContext();
|
||||
if (!this.audioContext) {
|
||||
console.warn('无法获取音频播放器的音频上下文');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建分析器节点
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('初始化音频分析器失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到音频播放器的输出节点
|
||||
*/
|
||||
connectToAudioPlayer() {
|
||||
try {
|
||||
// 获取音频播放器的流上下文
|
||||
const audioPlayer = window.chatApp?.audioPlayer;
|
||||
if (!audioPlayer || !audioPlayer.streamingContext) {
|
||||
console.warn('音频播放器或流上下文未初始化');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取音频播放器的流上下文
|
||||
const streamingContext = audioPlayer.streamingContext;
|
||||
|
||||
// 获取分析器节点
|
||||
const analyser = streamingContext.getAnalyser();
|
||||
if (!analyser) {
|
||||
console.warn('音频播放器尚未创建分析器节点,无法连接');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用音频播放器的分析器节点
|
||||
this.analyser = analyser;
|
||||
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('连接到音频播放器失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 嘴部动画循环
|
||||
*/
|
||||
animateMouth() {
|
||||
if (!this.isTalking) return;
|
||||
if (!this.live2dModel) return;
|
||||
const internal = this.live2dModel && this.live2dModel.internalModel;
|
||||
if (internal && internal.coreModel) {
|
||||
const coreModel = internal.coreModel;
|
||||
|
||||
// 获取音频分贝值
|
||||
let mouthValue = 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;
|
||||
}
|
||||
|
||||
// 确保嘴部参数在0-1范围内
|
||||
mouthValue = Math.min(Math.max(mouthValue, 0), 1);
|
||||
}
|
||||
coreModel.setParameterValueById(this.mouthParam, mouthValue);
|
||||
coreModel.update();
|
||||
}
|
||||
this.mouthAnimationId = requestAnimationFrame(() => this.animateMouth());
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始说话动画
|
||||
*/
|
||||
startTalking() {
|
||||
if (this.isTalking || !this.live2dModel) return;
|
||||
|
||||
// 确保音频分析器已初始化
|
||||
if (!this.analyser) {
|
||||
if (!this.initializeAudioAnalyzer()) {
|
||||
console.warn('音频分析器初始化失败,将使用模拟动画');
|
||||
// 即使分析器初始化失败,也启动动画(使用模拟数据)
|
||||
this.isTalking = true;
|
||||
this.animateMouth();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 连接到音频播放器输出
|
||||
if (!this.connectToAudioPlayer()) {
|
||||
console.warn('无法连接到音频播放器输出,将使用模拟动画');
|
||||
}
|
||||
|
||||
this.isTalking = true;
|
||||
this.animateMouth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止说话动画
|
||||
*/
|
||||
stopTalking() {
|
||||
this.isTalking = false;
|
||||
if (this.mouthAnimationId) {
|
||||
cancelAnimationFrame(this.mouthAnimationId);
|
||||
this.mouthAnimationId = null;
|
||||
}
|
||||
|
||||
// 重置嘴部参数
|
||||
if (this.live2dModel) {
|
||||
const internal = this.live2dModel.internalModel;
|
||||
if (internal && internal.coreModel) {
|
||||
const coreModel = internal.coreModel;
|
||||
coreModel.setParameterValueById(this.mouthParam, 0);
|
||||
coreModel.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于情绪触发动作
|
||||
* @param {string} emotion - 情绪名称
|
||||
*/
|
||||
triggerEmotionAction(emotion) {
|
||||
if (!this.live2dModel) return;
|
||||
|
||||
// 添加冷却时间控制,避免过于频繁触发
|
||||
const now = Date.now();
|
||||
if (this.lastEmotionActionTime && now - this.lastEmotionActionTime < 5000) { // 5秒冷却时间
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据情绪获取对应的动作
|
||||
const action = this.emotionToActionMap[emotion] || this.emotionToActionMap['default'];
|
||||
|
||||
// 触发动作并记录时间
|
||||
this.motion(action);
|
||||
this.lastEmotionActionTime = now;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 触发模型动作(Motion)
|
||||
* @param {string} name - 动作分组名称,如 'TapBody'、'FlickUp'、'Idle' 等
|
||||
*/
|
||||
motion(name) {
|
||||
try {
|
||||
if (!this.live2dModel) return;
|
||||
this.live2dModel.motion(name);
|
||||
} catch (error) {
|
||||
console.error('触发动作失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy() {
|
||||
this.stopTalking();
|
||||
|
||||
// 清理音频分析器
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
this.analyser = null;
|
||||
this.dataArray = null;
|
||||
|
||||
// 清理 Live2D 应用
|
||||
if (this.live2dApp) {
|
||||
this.live2dApp.destroy(true);
|
||||
this.live2dApp = null;
|
||||
}
|
||||
this.live2dModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出全局实例
|
||||
window.Live2DManager = Live2DManager;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,171 +1,490 @@
|
||||
// UI控制模块
|
||||
import { loadConfig, saveConfig } from '../config/manager.js';
|
||||
import { getAudioPlayer } from '../core/audio/player.js';
|
||||
import { loadConfig, saveConfig, getConfig } from '../config/manager.js';
|
||||
import { getAudioRecorder } from '../core/audio/recorder.js';
|
||||
import { getWebSocketHandler } from '../core/network/websocket.js';
|
||||
import { getAudioPlayer } from '../core/audio/player.js';
|
||||
|
||||
// UI控制器类
|
||||
export class UIController {
|
||||
class UIController {
|
||||
constructor() {
|
||||
this.isEditing = false;
|
||||
this.visualizerCanvas = null;
|
||||
this.visualizerContext = null;
|
||||
this.audioStatsTimer = null;
|
||||
this.currentBackgroundIndex = 0;
|
||||
this.backgroundImages = ['1.png', '2.png', '3.png'];
|
||||
|
||||
// 绑定方法
|
||||
this.init = this.init.bind(this);
|
||||
this.initEventListeners = this.initEventListeners.bind(this);
|
||||
this.updateDialButton = this.updateDialButton.bind(this);
|
||||
this.addChatMessage = this.addChatMessage.bind(this);
|
||||
this.switchBackground = this.switchBackground.bind(this);
|
||||
this.showModal = this.showModal.bind(this);
|
||||
this.hideModal = this.hideModal.bind(this);
|
||||
this.switchTab = this.switchTab.bind(this);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
init() {
|
||||
console.log('UIController init started');
|
||||
|
||||
this.visualizerCanvas = document.getElementById('audioVisualizer');
|
||||
this.visualizerContext = this.visualizerCanvas.getContext('2d');
|
||||
if (this.visualizerCanvas) {
|
||||
this.visualizerContext = this.visualizerCanvas.getContext('2d');
|
||||
this.initVisualizer();
|
||||
}
|
||||
|
||||
// 检查连接按钮在初始化时是否存在
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
console.log('connectBtn during init:', connectBtn);
|
||||
|
||||
this.initVisualizer();
|
||||
this.initEventListeners();
|
||||
this.startAudioStatsMonitor();
|
||||
loadConfig();
|
||||
|
||||
// 设置录音器回调
|
||||
const audioRecorder = getAudioRecorder();
|
||||
audioRecorder.onRecordingStart = (seconds) => {
|
||||
this.updateRecordButtonState(true, seconds);
|
||||
};
|
||||
|
||||
// 初始化状态显示
|
||||
this.updateConnectionUI(false);
|
||||
this.updateDialButton(false);
|
||||
|
||||
console.log('UIController init completed');
|
||||
}
|
||||
|
||||
// 初始化可视化器
|
||||
initVisualizer() {
|
||||
this.visualizerCanvas.width = this.visualizerCanvas.clientWidth;
|
||||
this.visualizerCanvas.height = this.visualizerCanvas.clientHeight;
|
||||
this.visualizerContext.fillStyle = '#fafafa';
|
||||
this.visualizerContext.fillRect(0, 0, this.visualizerCanvas.width, this.visualizerCanvas.height);
|
||||
if (this.visualizerCanvas) {
|
||||
this.visualizerCanvas.width = this.visualizerCanvas.clientWidth;
|
||||
this.visualizerCanvas.height = this.visualizerCanvas.clientHeight;
|
||||
this.visualizerContext.fillStyle = '#fafafa';
|
||||
this.visualizerContext.fillRect(0, 0, this.visualizerCanvas.width, this.visualizerCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态显示
|
||||
updateStatusDisplay(element, text) {
|
||||
element.textContent = text;
|
||||
element.removeAttribute('style');
|
||||
element.classList.remove('connected');
|
||||
if (text.includes('已连接')) {
|
||||
element.classList.add('connected');
|
||||
// 初始化事件监听器
|
||||
initEventListeners() {
|
||||
// 设置按钮
|
||||
const settingsBtn = document.getElementById('settingsBtn');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.addEventListener('click', () => {
|
||||
this.showModal('settingsModal');
|
||||
});
|
||||
}
|
||||
console.log('更新状态:', text, '类列表:', element.className, '样式属性:', element.getAttribute('style'));
|
||||
|
||||
// 背景切换按钮
|
||||
const backgroundBtn = document.getElementById('backgroundBtn');
|
||||
if (backgroundBtn) {
|
||||
backgroundBtn.addEventListener('click', this.switchBackground);
|
||||
}
|
||||
|
||||
// 拨号按钮
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
if (dialBtn) {
|
||||
dialBtn.addEventListener('click', () => {
|
||||
const wsHandler = getWebSocketHandler();
|
||||
const isConnected = wsHandler.isConnected();
|
||||
|
||||
if (isConnected) {
|
||||
wsHandler.disconnect();
|
||||
this.updateDialButton(false);
|
||||
} else {
|
||||
// 检查OTA地址是否已填写
|
||||
const otaUrlInput = document.getElementById('otaUrl');
|
||||
if (!otaUrlInput || !otaUrlInput.value.trim()) {
|
||||
// 如果OTA地址未填写,显示设置弹窗并切换到设备配置页
|
||||
this.showModal('settingsModal');
|
||||
this.switchTab('device');
|
||||
this.addChatMessage('请先填写OTA服务器地址', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行连接操作
|
||||
this.handleConnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 录音按钮
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
if (recordBtn) {
|
||||
recordBtn.addEventListener('click', () => {
|
||||
const audioRecorder = getAudioRecorder();
|
||||
if (audioRecorder.isRecording) {
|
||||
audioRecorder.stop();
|
||||
// 停止录音时移除录音样式
|
||||
recordBtn.classList.remove('recording');
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音';
|
||||
} else {
|
||||
// 先更新按钮状态为录音中
|
||||
recordBtn.classList.add('recording');
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音中';
|
||||
|
||||
// 延迟开始录音,确保按钮状态已更新
|
||||
setTimeout(() => {
|
||||
audioRecorder.start();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
const closeButtons = document.querySelectorAll('.close-btn');
|
||||
closeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const modal = e.target.closest('.modal');
|
||||
if (modal) {
|
||||
this.hideModal(modal.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 设置标签页切换
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
this.switchTab(e.target.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
// 点击模态框外部关闭
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
modals.forEach(modal => {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.hideModal(modal.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 保存配置按钮
|
||||
const saveConfigBtn = document.getElementById('saveConfigBtn');
|
||||
if (saveConfigBtn) {
|
||||
saveConfigBtn.addEventListener('click', () => {
|
||||
this.saveConfig();
|
||||
});
|
||||
}
|
||||
|
||||
// 添加MCP工具按钮
|
||||
const addMCPToolBtn = document.getElementById('addMCPToolBtn');
|
||||
if (addMCPToolBtn) {
|
||||
addMCPToolBtn.addEventListener('click', () => {
|
||||
this.addMCPTool();
|
||||
});
|
||||
}
|
||||
|
||||
// 连接按钮和取消按钮已被移除,功能已集成到拨号按钮中
|
||||
}
|
||||
|
||||
// 更新连接状态UI
|
||||
updateConnectionUI(isConnected) {
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
const otaStatus = document.getElementById('otaStatus');
|
||||
const connectButton = document.getElementById('connectButton');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendTextButton = document.getElementById('sendTextButton');
|
||||
const recordButton = document.getElementById('recordButton');
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
|
||||
if (isConnected) {
|
||||
this.updateStatusDisplay(connectionStatus, '● WS已连接');
|
||||
this.updateStatusDisplay(otaStatus, '● OTA已连接');
|
||||
connectButton.textContent = '断开';
|
||||
messageInput.disabled = false;
|
||||
sendTextButton.disabled = false;
|
||||
recordButton.disabled = false;
|
||||
} else {
|
||||
this.updateStatusDisplay(connectionStatus, '● WS未连接');
|
||||
this.updateStatusDisplay(otaStatus, '● OTA未连接');
|
||||
connectButton.textContent = '连接';
|
||||
messageInput.disabled = true;
|
||||
sendTextButton.disabled = true;
|
||||
recordButton.disabled = true;
|
||||
// 断开连接时,会话状态变为离线
|
||||
this.updateSessionStatus(null);
|
||||
if (connectionStatus) {
|
||||
if (isConnected) {
|
||||
connectionStatus.textContent = '已连接';
|
||||
if (statusDot) {
|
||||
statusDot.className = 'status-dot status-connected';
|
||||
}
|
||||
} else {
|
||||
connectionStatus.textContent = '离线';
|
||||
if (statusDot) {
|
||||
statusDot.className = 'status-dot status-disconnected';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新拨号按钮状态
|
||||
updateDialButton(isConnected) {
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
|
||||
if (dialBtn) {
|
||||
if (isConnected) {
|
||||
dialBtn.classList.add('dial-active');
|
||||
dialBtn.querySelector('.btn-text').textContent = '挂断';
|
||||
// 更新拨号按钮图标为挂断图标
|
||||
dialBtn.querySelector('svg').innerHTML = `
|
||||
<path d="M12,9C10.4,9 9,10.4 9,12C9,13.6 10.4,15 12,15C13.6,15 15,13.6 15,12C15,10.4 13.6,9 12,9M12,17C9.2,17 7,14.8 7,12C7,9.2 9.2,7 12,7C14.8,7 17,9.2 17,12C17,14.8 14.8,17 12,17M12,4.5C7,4.5 2.7,7.6 1,12C2.7,16.4 7,19.5 12,19.5C17,19.5 21.3,16.4 23,12C21.3,7.6 17,4.5 12,4.5Z"/>
|
||||
`;
|
||||
} else {
|
||||
dialBtn.classList.remove('dial-active');
|
||||
dialBtn.querySelector('.btn-text').textContent = '拨号';
|
||||
// 恢复拨号按钮图标
|
||||
dialBtn.querySelector('svg').innerHTML = `
|
||||
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新录音按钮状态
|
||||
if (recordBtn) {
|
||||
if (isConnected) {
|
||||
recordBtn.disabled = false;
|
||||
recordBtn.title = '开始录音';
|
||||
// 确保录音按钮恢复到正常状态
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音';
|
||||
recordBtn.classList.remove('recording');
|
||||
} else {
|
||||
recordBtn.disabled = true;
|
||||
recordBtn.title = '请先连接服务器';
|
||||
// 确保录音按钮恢复到正常状态
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音';
|
||||
recordBtn.classList.remove('recording');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新录音按钮状态
|
||||
updateRecordButtonState(isRecording, seconds = 0) {
|
||||
const recordButton = document.getElementById('recordButton');
|
||||
if (isRecording) {
|
||||
recordButton.textContent = `停止录音 ${seconds.toFixed(1)}秒`;
|
||||
recordButton.classList.add('recording');
|
||||
} else {
|
||||
recordButton.textContent = '开始录音';
|
||||
recordButton.classList.remove('recording');
|
||||
}
|
||||
recordButton.disabled = false;
|
||||
}
|
||||
|
||||
// 更新会话状态UI
|
||||
updateSessionStatus(isSpeaking) {
|
||||
const sessionStatus = document.getElementById('sessionStatus');
|
||||
if (!sessionStatus) return;
|
||||
|
||||
// 保留背景元素
|
||||
const bgHtml = '<span id="sessionStatusBg" style="position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, rgba(76, 175, 80, 0.2), rgba(33, 150, 243, 0.2)); transition: width 0.15s ease-out, background 0.3s ease; z-index: 0; border-radius: 20px;"></span>';
|
||||
|
||||
if (isSpeaking === null) {
|
||||
// 离线状态
|
||||
sessionStatus.innerHTML = bgHtml + '<span style="position: relative; z-index: 1;"><span class="emoji-large">😶</span> 小智离线中</span>';
|
||||
sessionStatus.className = 'status offline';
|
||||
} else if (isSpeaking) {
|
||||
// 说话中
|
||||
sessionStatus.innerHTML = bgHtml + '<span style="position: relative; z-index: 1;"><span class="emoji-large">😶</span> 小智说话中</span>';
|
||||
sessionStatus.className = 'status speaking';
|
||||
} else {
|
||||
// 聆听中
|
||||
sessionStatus.innerHTML = bgHtml + '<span style="position: relative; z-index: 1;"><span class="emoji-large">😶</span> 小智聆听中</span>';
|
||||
sessionStatus.className = 'status listening';
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
if (recordBtn) {
|
||||
if (isRecording) {
|
||||
recordBtn.querySelector('.btn-text').textContent = `录音中 ${seconds.toFixed(1)}秒`;
|
||||
recordBtn.classList.add('recording');
|
||||
} else {
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音';
|
||||
recordBtn.classList.remove('recording');
|
||||
}
|
||||
recordBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新会话表情
|
||||
updateSessionEmotion(emoji) {
|
||||
const sessionStatus = document.getElementById('sessionStatus');
|
||||
if (!sessionStatus) return;
|
||||
// 添加聊天消息
|
||||
addChatMessage(content, isUser = false) {
|
||||
const chatStream = document.getElementById('chatStream');
|
||||
if (!chatStream) return;
|
||||
|
||||
// 获取当前文本内容,提取非表情部分
|
||||
let currentText = sessionStatus.textContent;
|
||||
// 移除现有的表情符号
|
||||
currentText = currentText.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim();
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `chat-message ${isUser ? 'user' : 'ai'}`;
|
||||
messageDiv.innerHTML = `<div class="message-bubble">${content}</div>`;
|
||||
chatStream.appendChild(messageDiv);
|
||||
|
||||
// 保留背景元素
|
||||
const bgHtml = '<span id="sessionStatusBg" style="position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, rgba(76, 175, 80, 0.2), rgba(33, 150, 243, 0.2)); transition: width 0.15s ease-out, background 0.3s ease; z-index: 0; border-radius: 20px;"></span>';
|
||||
// 自动滚动到底部
|
||||
chatStream.scrollTop = chatStream.scrollHeight;
|
||||
}
|
||||
|
||||
// 使用 innerHTML 添加带样式的表情
|
||||
sessionStatus.innerHTML = bgHtml + `<span style="position: relative; z-index: 1;"><span class="emoji-large">${emoji}</span> ${currentText}</span>`;
|
||||
// 切换背景
|
||||
switchBackground() {
|
||||
this.currentBackgroundIndex = (this.currentBackgroundIndex + 1) % this.backgroundImages.length;
|
||||
const backgroundContainer = document.querySelector('.background-container');
|
||||
if (backgroundContainer) {
|
||||
backgroundContainer.style.backgroundImage = `url('../images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
showModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏模态框
|
||||
hideModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
switchTab(tabName) {
|
||||
// 移除所有标签页的active类
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
tabBtns.forEach(btn => btn.classList.remove('active'));
|
||||
tabContents.forEach(content => content.classList.remove('active'));
|
||||
|
||||
// 激活选中的标签页
|
||||
const activeTabBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
const activeTabContent = document.getElementById(`${tabName}Tab`);
|
||||
|
||||
if (activeTabBtn && activeTabContent) {
|
||||
activeTabBtn.classList.add('active');
|
||||
activeTabContent.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
saveConfig() {
|
||||
const config = {
|
||||
serverUrl: document.getElementById('serverUrl').value,
|
||||
serverPort: document.getElementById('serverPort').value,
|
||||
audioDevice: document.getElementById('audioDevice').value,
|
||||
audioSampleRate: document.getElementById('audioSampleRate').value,
|
||||
audioChannels: document.getElementById('audioChannels').value
|
||||
};
|
||||
|
||||
saveConfig(config);
|
||||
this.hideModal('settingsModal');
|
||||
|
||||
// 显示保存成功消息
|
||||
this.addChatMessage('配置已保存', false);
|
||||
}
|
||||
|
||||
// 处理连接按钮点击
|
||||
async handleConnect() {
|
||||
console.log('handleConnect called');
|
||||
|
||||
// 确保切换到设备配置标签页
|
||||
this.switchTab('device');
|
||||
|
||||
// 等待DOM更新
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const otaUrlInput = document.getElementById('otaUrl');
|
||||
|
||||
console.log('otaUrl element:', otaUrlInput);
|
||||
|
||||
if (!otaUrlInput || !otaUrlInput.value) {
|
||||
this.addChatMessage('请输入OTA服务器地址', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const otaUrl = otaUrlInput.value;
|
||||
console.log('otaUrl value:', otaUrl);
|
||||
|
||||
// 更新拨号按钮状态为连接中
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
if (dialBtn) {
|
||||
dialBtn.classList.add('dial-active');
|
||||
dialBtn.querySelector('.btn-text').textContent = '连接中...';
|
||||
dialBtn.disabled = true;
|
||||
}
|
||||
|
||||
// 显示连接中消息
|
||||
this.addChatMessage('正在连接服务器...', false);
|
||||
|
||||
try {
|
||||
// 获取配置信息
|
||||
const config = getConfig();
|
||||
|
||||
// 导入OTA连接器
|
||||
const { webSocketConnect } = await import('../core/network/ota-connector.js');
|
||||
|
||||
// 建立OTA连接
|
||||
const websocket = await webSocketConnect(otaUrl, config);
|
||||
|
||||
if (websocket) {
|
||||
// 获取WebSocket处理器
|
||||
const wsHandler = getWebSocketHandler();
|
||||
|
||||
// 设置WebSocket连接
|
||||
wsHandler.websocket = websocket;
|
||||
|
||||
// 设置连接状态回调
|
||||
wsHandler.onConnectionStateChange = (isConnected) => {
|
||||
this.updateConnectionUI(isConnected);
|
||||
this.updateDialButton(isConnected);
|
||||
};
|
||||
|
||||
// 设置聊天消息回调
|
||||
wsHandler.onChatMessage = (text, isUser) => {
|
||||
this.addChatMessage(text, isUser);
|
||||
};
|
||||
|
||||
// 设置录音按钮状态回调
|
||||
wsHandler.onRecordButtonStateChange = (isRecording) => {
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
if (recordBtn) {
|
||||
if (isRecording) {
|
||||
recordBtn.classList.add('recording');
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音中';
|
||||
} else {
|
||||
recordBtn.classList.remove('recording');
|
||||
recordBtn.querySelector('.btn-text').textContent = '录音';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 连接成功
|
||||
this.addChatMessage('OTA连接成功,正在建立WebSocket连接...', false);
|
||||
|
||||
// 更新拨号按钮状态
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
if (dialBtn) {
|
||||
dialBtn.disabled = false;
|
||||
dialBtn.querySelector('.btn-text').textContent = '挂断';
|
||||
dialBtn.classList.add('dial-active');
|
||||
}
|
||||
|
||||
this.hideModal('settingsModal');
|
||||
|
||||
// 自动尝试建立WebSocket连接
|
||||
setTimeout(() => {
|
||||
wsHandler.connect();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error('OTA连接失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
});
|
||||
|
||||
// 显示错误消息
|
||||
const errorMessage = error.message.includes('Cannot set properties of null')
|
||||
? '连接失败:请刷新页面重试'
|
||||
: `连接失败: ${error.message}`;
|
||||
|
||||
this.addChatMessage(errorMessage, false);
|
||||
|
||||
// 恢复拨号按钮状态
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
if (dialBtn) {
|
||||
dialBtn.disabled = false;
|
||||
dialBtn.querySelector('.btn-text').textContent = '拨号';
|
||||
dialBtn.classList.remove('dial-active');
|
||||
console.log('Dial button state restored successfully');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加MCP工具
|
||||
addMCPTool() {
|
||||
const mcpToolsList = document.getElementById('mcpToolsList');
|
||||
if (!mcpToolsList) return;
|
||||
|
||||
const toolId = `mcp-tool-${Date.now()}`;
|
||||
const toolDiv = document.createElement('div');
|
||||
toolDiv.className = 'properties-container';
|
||||
toolDiv.innerHTML = `
|
||||
<div class="property-item">
|
||||
<input type="text" placeholder="工具名称" value="新工具">
|
||||
<input type="text" placeholder="工具描述" value="工具描述">
|
||||
<button class="remove-property" onclick="uiController.removeMCPTool('${toolId}')">删除</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mcpToolsList.appendChild(toolDiv);
|
||||
}
|
||||
|
||||
// 移除MCP工具
|
||||
removeMCPTool(toolId) {
|
||||
const toolElement = document.getElementById(toolId);
|
||||
if (toolElement) {
|
||||
toolElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新音频统计信息
|
||||
updateAudioStats() {
|
||||
const audioPlayer = getAudioPlayer();
|
||||
if (!audioPlayer) return;
|
||||
|
||||
const stats = audioPlayer.getAudioStats();
|
||||
|
||||
const sessionStatus = document.getElementById('sessionStatus');
|
||||
const sessionStatusBg = document.getElementById('sessionStatusBg');
|
||||
|
||||
// 只在说话状态下显示背景进度
|
||||
if (sessionStatus && sessionStatus.classList.contains('speaking') && sessionStatusBg) {
|
||||
if (stats.pendingPlay > 0) {
|
||||
// 计算进度:5包=50%,10包及以上=100%
|
||||
let percentage;
|
||||
if (stats.pendingPlay >= 10) {
|
||||
percentage = 100;
|
||||
} else {
|
||||
percentage = (stats.pendingPlay / 10) * 100;
|
||||
}
|
||||
|
||||
sessionStatusBg.style.width = `${percentage}%`;
|
||||
|
||||
// 根据缓冲量改变背景颜色
|
||||
if (stats.pendingPlay < 5) {
|
||||
// 缓冲不足:橙红色半透明
|
||||
sessionStatusBg.style.background = 'linear-gradient(90deg, rgba(255, 152, 0, 0.25), rgba(255, 87, 34, 0.25))';
|
||||
} else if (stats.pendingPlay < 10) {
|
||||
// 一般:黄绿色半透明
|
||||
sessionStatusBg.style.background = 'linear-gradient(90deg, rgba(205, 220, 57, 0.25), rgba(76, 175, 80, 0.25))';
|
||||
} else {
|
||||
// 充足:绿蓝色半透明
|
||||
sessionStatusBg.style.background = 'linear-gradient(90deg, rgba(76, 175, 80, 0.25), rgba(33, 150, 243, 0.25))';
|
||||
}
|
||||
} else {
|
||||
// 没有缓冲,隐藏背景
|
||||
sessionStatusBg.style.width = '0%';
|
||||
}
|
||||
} else {
|
||||
// 非说话状态,隐藏背景
|
||||
if (sessionStatusBg) {
|
||||
sessionStatusBg.style.width = '0%';
|
||||
}
|
||||
}
|
||||
// 这里可以添加音频统计的UI更新逻辑
|
||||
}
|
||||
|
||||
// 启动音频统计监控
|
||||
@@ -186,6 +505,8 @@ export class UIController {
|
||||
|
||||
// 绘制音频可视化效果
|
||||
drawVisualizer(dataArray) {
|
||||
if (!this.visualizerContext || !this.visualizerCanvas) return;
|
||||
|
||||
this.visualizerContext.fillStyle = '#fafafa';
|
||||
this.visualizerContext.fillRect(0, 0, this.visualizerCanvas.width, this.visualizerCanvas.height);
|
||||
|
||||
@@ -197,146 +518,32 @@ export class UIController {
|
||||
barHeight = dataArray[i] / 2;
|
||||
|
||||
// 创建渐变色:从紫色到蓝色到青色
|
||||
const hue = 200 + (barHeight / this.visualizerCanvas.height) * 60; // 200-260度,从青色到紫色
|
||||
const saturation = 80 + (barHeight / this.visualizerCanvas.height) * 20; // 饱和度 80-100%
|
||||
const lightness = 45 + (barHeight / this.visualizerCanvas.height) * 15; // 亮度 45-60%
|
||||
const gradient = this.visualizerContext.createLinearGradient(0, 0, 0, this.visualizerCanvas.height);
|
||||
gradient.addColorStop(0, '#8e44ad');
|
||||
gradient.addColorStop(0.5, '#3498db');
|
||||
gradient.addColorStop(1, '#1abc9c');
|
||||
|
||||
this.visualizerContext.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||
this.visualizerContext.fillStyle = gradient;
|
||||
this.visualizerContext.fillRect(x, this.visualizerCanvas.height - barHeight, barWidth, barHeight);
|
||||
|
||||
x += barWidth + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
initEventListeners() {
|
||||
const wsHandler = getWebSocketHandler();
|
||||
const audioRecorder = getAudioRecorder();
|
||||
// 更新会话状态UI
|
||||
updateSessionStatus(isSpeaking) {
|
||||
// 这里可以添加会话状态的UI更新逻辑
|
||||
// 例如:更新Live2D角色的表情或状态指示器
|
||||
}
|
||||
|
||||
// 设置WebSocket回调
|
||||
wsHandler.onConnectionStateChange = (isConnected) => {
|
||||
this.updateConnectionUI(isConnected);
|
||||
};
|
||||
|
||||
wsHandler.onRecordButtonStateChange = (isRecording) => {
|
||||
this.updateRecordButtonState(isRecording);
|
||||
};
|
||||
|
||||
wsHandler.onSessionStateChange = (isSpeaking) => {
|
||||
this.updateSessionStatus(isSpeaking);
|
||||
};
|
||||
|
||||
wsHandler.onSessionEmotionChange = (emoji) => {
|
||||
this.updateSessionEmotion(emoji);
|
||||
};
|
||||
|
||||
// 设置录音器回调
|
||||
audioRecorder.onRecordingStart = (seconds) => {
|
||||
this.updateRecordButtonState(true, seconds);
|
||||
};
|
||||
|
||||
audioRecorder.onRecordingStop = () => {
|
||||
this.updateRecordButtonState(false);
|
||||
};
|
||||
|
||||
audioRecorder.onVisualizerUpdate = (dataArray) => {
|
||||
this.drawVisualizer(dataArray);
|
||||
};
|
||||
|
||||
// 连接按钮
|
||||
const connectButton = document.getElementById('connectButton');
|
||||
let isConnecting = false;
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (isConnecting) return;
|
||||
|
||||
if (wsHandler.isConnected()) {
|
||||
wsHandler.disconnect();
|
||||
} else {
|
||||
isConnecting = true;
|
||||
await wsHandler.connect();
|
||||
isConnecting = false;
|
||||
}
|
||||
};
|
||||
|
||||
connectButton.addEventListener('click', handleConnect);
|
||||
|
||||
// 设备配置面板编辑/确定切换
|
||||
const toggleButton = document.getElementById('toggleConfig');
|
||||
const deviceMacInput = document.getElementById('deviceMac');
|
||||
const deviceNameInput = document.getElementById('deviceName');
|
||||
const clientIdInput = document.getElementById('clientId');
|
||||
|
||||
toggleButton.addEventListener('click', () => {
|
||||
this.isEditing = !this.isEditing;
|
||||
|
||||
deviceMacInput.disabled = !this.isEditing;
|
||||
deviceNameInput.disabled = !this.isEditing;
|
||||
clientIdInput.disabled = !this.isEditing;
|
||||
|
||||
toggleButton.textContent = this.isEditing ? '确定' : '编辑';
|
||||
|
||||
if (!this.isEditing) {
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
// 标签页切换
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
|
||||
tab.classList.add('active');
|
||||
const tabContent = document.getElementById(`${tab.dataset.tab}Tab`);
|
||||
tabContent.classList.add('active');
|
||||
|
||||
if (tab.dataset.tab === 'voice') {
|
||||
setTimeout(() => {
|
||||
this.initVisualizer();
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 发送文本消息
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendTextButton = document.getElementById('sendTextButton');
|
||||
|
||||
const sendMessage = () => {
|
||||
const message = messageInput.value.trim();
|
||||
if (message && wsHandler.sendTextMessage(message)) {
|
||||
messageInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
sendTextButton.addEventListener('click', sendMessage);
|
||||
messageInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
});
|
||||
|
||||
// 录音按钮
|
||||
const recordButton = document.getElementById('recordButton');
|
||||
recordButton.addEventListener('click', () => {
|
||||
if (audioRecorder.isRecording) {
|
||||
audioRecorder.stop();
|
||||
} else {
|
||||
audioRecorder.start();
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口大小变化
|
||||
window.addEventListener('resize', () => this.initVisualizer());
|
||||
// 更新会话表情
|
||||
updateSessionEmotion(emoji) {
|
||||
// 这里可以添加表情更新的逻辑
|
||||
// 例如:在状态指示器中显示表情
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例
|
||||
let uiControllerInstance = null;
|
||||
// 创建全局实例
|
||||
export const uiController = new UIController();
|
||||
|
||||
export function getUIController() {
|
||||
if (!uiControllerInstance) {
|
||||
uiControllerInstance = new UIController();
|
||||
}
|
||||
return uiControllerInstance;
|
||||
}
|
||||
// 导出类供其他模块使用
|
||||
export { UIController };
|
||||
@@ -13,12 +13,15 @@ let visualizerCanvas = document.getElementById('audioVisualizer');
|
||||
|
||||
// ota 是否连接成功,修改成对应的样式
|
||||
export function otaStatusStyle(flan) {
|
||||
if (flan) {
|
||||
document.getElementById('otaStatus').textContent = 'OTA已连接';
|
||||
document.getElementById('otaStatus').style.color = 'green';
|
||||
} else {
|
||||
document.getElementById('otaStatus').textContent = 'OTA未连接';
|
||||
document.getElementById('otaStatus').style.color = 'red';
|
||||
const otaStatusElement = document.getElementById('otaStatus');
|
||||
if (otaStatusElement) {
|
||||
if (flan) {
|
||||
otaStatusElement.textContent = 'OTA已连接';
|
||||
otaStatusElement.style.color = 'green';
|
||||
} else {
|
||||
otaStatusElement.textContent = 'OTA未连接';
|
||||
otaStatusElement.style.color = 'red';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { getLogContainer } from '../ui/dom-helper.js'
|
||||
|
||||
const logContainer = getLogContainer();
|
||||
// 日志记录函数
|
||||
export function log(message, type = 'info') {
|
||||
// 将消息按换行符分割成多行
|
||||
@@ -8,6 +5,15 @@ export function log(message, type = 'info') {
|
||||
const now = new Date();
|
||||
// const timestamp = `[${now.toLocaleTimeString()}] `;
|
||||
const timestamp = `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, '0')}] `;
|
||||
|
||||
// 检查是否存在日志容器
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
if (!logContainer) {
|
||||
// 如果日志容器不存在,只输出到控制台
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 为每一行创建日志条目
|
||||
lines.forEach((line, index) => {
|
||||
const logEntry = document.createElement('div');
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
// 检测是否使用file://协议打开
|
||||
if (window.location.protocol === 'file:') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 创建背景模糊遮罩
|
||||
const overlayDiv = document.createElement('div');
|
||||
overlayDiv.className = 'file-protocol-overlay';
|
||||
document.body.appendChild(overlayDiv);
|
||||
|
||||
// 创建警告框
|
||||
const warningDiv = document.createElement('div');
|
||||
warningDiv.id = 'fileProtocolWarning';
|
||||
@@ -32,198 +37,216 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 背景容器 -->
|
||||
<div class="background-container">
|
||||
<div class="background-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="container">
|
||||
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
||||
正在加载Opus库...
|
||||
<!-- 连接状态指示器 - 页面顶部中部 -->
|
||||
<div class="connection-status-top">
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot status-disconnected"></span>
|
||||
<span id="connectionStatus">离线</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 两栏布局:设备配置和连接信息 -->
|
||||
<div class="two-column-layout">
|
||||
<!-- 设备配置面板 -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
设备配置
|
||||
<button class="toggle-button" id="toggleConfig">编辑</button>
|
||||
</h2>
|
||||
<div class="config-panel" id="configPanel">
|
||||
<div class="control-panel">
|
||||
<div class="config-row">
|
||||
<div class="config-item">
|
||||
<label for="deviceMac">设备MAC:</label>
|
||||
<input type="text" id="deviceMac" placeholder="device-id" disabled>
|
||||
<!-- Live2D Canvas 容器 -->
|
||||
<canvas id="live2d-stage"></canvas>
|
||||
|
||||
<!-- 聊天消息流(弹幕样式) -->
|
||||
<div class="chat-stream" id="chatStream">
|
||||
<!-- 聊天消息将动态插入这里 -->
|
||||
</div>
|
||||
|
||||
<!-- 底部控制栏 -->
|
||||
<div class="control-bar">
|
||||
<button class="control-btn" id="settingsBtn" title="设置">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5A3.5 3.5 0 0 1 15.5 12A3.5 3.5 0 0 1 12 15.5M19.43 12.97C19.47 12.65 19.5 12.33 19.5 12C19.5 11.67 19.47 11.34 19.43 11L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.54 5.05 19.27 4.96 19.05 5.05L16.56 6.05C16.04 5.66 15.5 5.32 14.87 5.07L14.5 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.5 2.42L9.13 5.07C8.5 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.73 4.96 4.46 5.05 4.34 5.27L2.34 8.73C2.22 8.95 2.27 9.22 2.46 9.37L4.57 11C4.53 11.34 4.5 11.67 4.5 12C4.5 12.33 4.53 12.65 4.57 12.97L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.46 18.95 4.73 19.03 4.95 18.95L7.44 17.94C7.96 18.34 8.5 18.68 9.13 18.93L9.5 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.5 21.58L14.87 18.93C15.5 18.68 16.04 18.34 16.56 17.94L19.05 18.95C19.27 19.03 19.54 18.95 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.97Z" />
|
||||
</svg>
|
||||
<span class="btn-text">设置</span>
|
||||
</button>
|
||||
|
||||
<button class="control-btn" id="backgroundBtn" title="切换背景">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15V18M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
|
||||
</svg>
|
||||
<span class="btn-text">背景</span>
|
||||
</button>
|
||||
|
||||
<button class="control-btn dial-btn" id="dialBtn" title="拨号">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" />
|
||||
</svg>
|
||||
<span class="btn-text">拨号</span>
|
||||
</button>
|
||||
|
||||
<button class="control-btn" id="recordBtn" title="开始录音" disabled>
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" />
|
||||
</svg>
|
||||
<span class="btn-text">录音</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置弹窗 -->
|
||||
<div class="modal" id="settingsModal">
|
||||
<div class="modal-content settings-modal">
|
||||
<div class="modal-header">
|
||||
<h2>设置</h2>
|
||||
<button class="close-btn" id="closeSettingsBtn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="settings-tabs">
|
||||
<button class="tab-btn active" data-tab="device">设备配置</button>
|
||||
<button class="tab-btn" data-tab="mcp">MCP工具</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="deviceTab">
|
||||
<!-- 设备配置面板 -->
|
||||
<div class="config-panel">
|
||||
<div class="control-panel">
|
||||
<div class="config-row">
|
||||
<div class="config-item">
|
||||
<label for="deviceMac">设备MAC:</label>
|
||||
<input type="text" id="deviceMac" placeholder="device-id" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label for="clientId">客户端ID:</label>
|
||||
<input type="text" id="clientId" value="web_test_client" placeholder="client-id"
|
||||
disabled>
|
||||
<div class="config-row">
|
||||
<div class="config-item">
|
||||
<label for="clientId">客户端ID:</label>
|
||||
<input type="text" id="clientId" value="web_test_client" placeholder="client-id">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label for="deviceName">设备名称:</label>
|
||||
<input type="text" id="deviceName" value="Web测试设备" placeholder="deviceName">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<div class="config-item">
|
||||
<label for="deviceName">设备名称:</label>
|
||||
<input type="text" id="deviceName" value="Web测试设备" placeholder="deviceName" disabled>
|
||||
</div>
|
||||
|
||||
<!-- 连接信息面板 -->
|
||||
<div class="connection-controls">
|
||||
<div class="input-group">
|
||||
<label for="otaUrl">OTA服务器地址:</label>
|
||||
<input type="text" id="otaUrl" value="http://127.0.0.1:8002/xiaozhi/ota/"
|
||||
placeholder="OTA服务器地址,如:http://127.0.0.1:8002/xiaozhi/ota/" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="serverUrl">WebSocket服务器地址:</label>
|
||||
<input type="text" id="serverUrl" value="" readonly disabled
|
||||
placeholder="填写OTA地址后,点击拨号按钮自动连接" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="mcpTab">
|
||||
<!-- MCP 工具管理区域 -->
|
||||
<div class="mcp-tools-container">
|
||||
<div class="mcp-tools-header">
|
||||
<h3>MCP 工具管理</h3>
|
||||
<button class="btn-secondary" id="toggleMcpTools">
|
||||
收起
|
||||
</button>
|
||||
</div>
|
||||
<div class="mcp-tools-panel" id="mcpToolsPanel">
|
||||
<div class="mcp-tools-list" id="mcpToolsContainer">
|
||||
<!-- 工具列表将动态插入这里 -->
|
||||
</div>
|
||||
<div class="mcp-actions">
|
||||
<button class="btn-primary" id="addMcpToolBtn">
|
||||
➕ 添加新工具
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接信息面板 -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
连接信息
|
||||
<span class="connection-status">
|
||||
<span id="otaStatus" class="status">● OTA未连接</span>
|
||||
<span id="connectionStatus" class="status">● WS未连接</span>
|
||||
</span>
|
||||
<button class="connect-button" id="connectButton">
|
||||
<span>●</span>
|
||||
连接
|
||||
</button>
|
||||
</h2>
|
||||
<div class="connection-controls">
|
||||
<input type="text" id="otaUrl" value="http://127.0.0.1:8002/xiaozhi/ota/"
|
||||
placeholder="OTA服务器地址,如:http://127.0.0.1:8002/xiaozhi/ota/" />
|
||||
<input type="text" id="serverUrl" value="" readonly disabled placeholder="点击连接按钮后,自动从OTA接口获取" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="text">文本消息</button>
|
||||
<button class="tab" data-tab="voice">语音消息</button>
|
||||
<!-- MCP 工具编辑模态框 -->
|
||||
<div id="mcpToolModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="mcpModalTitle">添加工具</h2>
|
||||
<button class="close-btn" id="closeMcpModalBtn">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="textTab">
|
||||
<div class="message-input">
|
||||
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
|
||||
<button id="sendTextButton" disabled>发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="voiceTab">
|
||||
<div class="audio-controls">
|
||||
<button id="recordButton" class="record-button" disabled>开始录音</button>
|
||||
</div>
|
||||
<canvas id="audioVisualizer" class="audio-visualizer"></canvas>
|
||||
</div>
|
||||
|
||||
<div style="margin: -10px 0px 10px 0px;">
|
||||
<span class="connection-status llm-emoji">
|
||||
<span id="sessionStatus" class="status offline" style="position: relative; display: inline-block;">
|
||||
<span id="sessionStatusBg"
|
||||
style="position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, rgba(76, 175, 80, 0.2), rgba(33, 150, 243, 0.2)); transition: width 0.15s ease-out, background 0.3s ease; z-index: 0; border-radius: 20px;"></span>
|
||||
<span style="position: relative; z-index: 1;"><span class="emoji-large">😶</span> 小智离线中</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div id="conversation" class="conversation"></div>
|
||||
<div id="logContainer">
|
||||
<div class="log-entry log-info">准备就绪,请连接服务器开始测试...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP 工具管理区域 -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
MCP 工具管理
|
||||
<span class="connection-status">
|
||||
<span id="mcpToolsCount">0 个工具</span>
|
||||
</span>
|
||||
<button class="toggle-button" id="toggleMcpTools">展开</button>
|
||||
</h2>
|
||||
<div class="config-panel" id="mcpToolsPanel">
|
||||
<div id="mcpToolsContainer" class="mcp-tools-container">
|
||||
<!-- 工具列表将动态插入这里 -->
|
||||
</div>
|
||||
<div style="text-align: center; padding: 15px 0;">
|
||||
<button class="btn" id="addMcpToolBtn" style="background-color: #4caf50;">
|
||||
➕ 添加新工具
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP 工具编辑模态框 -->
|
||||
<div id="mcpToolModal"
|
||||
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; overflow-y: auto;">
|
||||
<div
|
||||
style="background-color: white; border-radius: 10px; padding: 25px; width: 90%; max-width: 700px; margin: 50px auto; max-height: 85vh; overflow-y: auto;">
|
||||
<div
|
||||
style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #e0e0e0;">
|
||||
<h2 id="mcpModalTitle" style="font-size: 20px; font-weight: bold; color: #333; margin: 0;">添加工具</h2>
|
||||
<button id="closeMcpModalBtn"
|
||||
style="background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px;">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div id="mcpErrorContainer"></div>
|
||||
|
||||
<form id="mcpToolForm">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label
|
||||
style="display: block; margin-bottom: 8px; font-weight: 500; color: #333; font-size: 14px;">工具名称
|
||||
*</label>
|
||||
<input type="text" id="mcpToolName" placeholder="例如: self.get_device_status" required
|
||||
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
|
||||
<div class="input-group">
|
||||
<label for="mcpToolName">工具名称 *</label>
|
||||
<input type="text" id="mcpToolName" placeholder="例如: self.get_device_status" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label
|
||||
style="display: block; margin-bottom: 8px; font-weight: 500; color: #333; font-size: 14px;">工具描述
|
||||
*</label>
|
||||
<textarea id="mcpToolDescription" placeholder="详细描述工具的功能和使用场景..." required
|
||||
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; min-height: 80px; resize: vertical;"></textarea>
|
||||
<div class="input-group">
|
||||
<label for="mcpToolDescription">工具描述 *</label>
|
||||
<textarea id="mcpToolDescription" placeholder="详细描述工具的功能和使用场景..." required></textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label
|
||||
style="display: block; margin-bottom: 8px; font-weight: 500; color: #333; font-size: 14px;">输入参数</label>
|
||||
<div
|
||||
style="background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; padding: 15px;">
|
||||
<div class="input-group">
|
||||
<label>输入参数</label>
|
||||
<div class="properties-container">
|
||||
<div id="mcpPropertiesContainer">
|
||||
<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">
|
||||
暂无参数,点击下方按钮添加参数</div>
|
||||
暂无参数,点击下方按钮添加参数
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="addMcpPropertyBtn"
|
||||
style="width: 100%; margin-top: 10px; padding: 8px 15px; border: none; border-radius: 5px; background-color: #2196f3; color: white; cursor: pointer; font-size: 14px;">
|
||||
<button type="button" class="btn-secondary" id="addMcpPropertyBtn">
|
||||
➕ 添加参数
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label
|
||||
style="display: block; margin-bottom: 8px; font-weight: 500; color: #333; font-size: 14px;">
|
||||
<div class="input-group">
|
||||
<label for="mcpMockResponse">
|
||||
模拟返回结果 (JSON 格式,可选)
|
||||
<span style="font-size: 12px; color: #999; font-weight: normal;">- 留空则返回默认成功消息</span>
|
||||
</label>
|
||||
<textarea id="mcpMockResponse" placeholder='{"success": true, "data": "执行成功"}'
|
||||
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 13px; font-family: 'Courier New', monospace; min-height: 100px; resize: vertical;"></textarea>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 4px;">
|
||||
💡 提示:如果设置了模拟返回结果,工具调用时将返回这个 JSON 对象
|
||||
</div>
|
||||
<textarea id="mcpMockResponse" placeholder='{"success": true, "data": "执行成功"}'></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 25px;">
|
||||
<button type="button" id="cancelMcpBtn"
|
||||
style="padding: 8px 15px; border: none; border-radius: 5px; background-color: #9e9e9e; color: white; cursor: pointer; font-size: 14px;">取消</button>
|
||||
<button type="submit"
|
||||
style="padding: 8px 15px; border: none; border-radius: 5px; background-color: #4caf50; color: white; cursor: pointer; font-size: 14px;">保存</button>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" id="cancelMcpBtn">取消</button>
|
||||
<button type="submit" class="btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PIXI.js 2D渲染引擎 -->
|
||||
<script src="js/live2d/pixi.js"></script>
|
||||
|
||||
<!-- Live2D Cubism 4.0 SDK -->
|
||||
<script src="js/live2d/live2dcubismcore.min.js"></script>
|
||||
<script src="js/live2d/cubism4.min.js"></script>
|
||||
|
||||
<!-- Live2D 管理器 -->
|
||||
<script src="js/live2d/live2d.js"></script>
|
||||
|
||||
<!-- Opus解码库 -->
|
||||
<script src="js/utils/libopus.js"></script>
|
||||
|
||||
<!-- 主应用入口 -->
|
||||
<script type="module" src="js/app.js"></script>
|
||||
|
||||
<!-- 全局错误处理 -->
|
||||
<script>
|
||||
// 添加全局错误处理
|
||||
window.addEventListener('error', function (event) {
|
||||
console.error('全局错误:', event.error);
|
||||
});
|
||||
|
||||
// 添加未处理的Promise拒绝处理
|
||||
window.addEventListener('unhandledrejection', function (event) {
|
||||
console.error('未处理的Promise拒绝:', event.reason);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user