├── LLM_env ├── .gitkeep ├── conversation_history.txt └── LM Studio │ └── LM Studio.htm ├── Live2d_env ├── .gitkeep ├── running_photo.jpg ├── pachirisu anime girl - top half.moc3 ├── pachirisu anime girl - top half.4096 │ └── texture_00.png ├── .model3.json ├── pachirisu anime girl - top half.model3.json ├── pachirisu anime girl - top half.cdi3.json └── pachirisu anime girl - top half.physics3.json ├── TTS_env ├── tmp │ └── .gitkeep ├── CosyVoice │ └── .gitkeep ├── voice_history │ ├── .gitkeep │ ├── 20250227_231810.wav │ ├── 20250228_033731.wav │ └── 20250228_042517.wav ├── voice_training_sample │ ├── .gitkeep │ ├── text_taiyuan.txt │ ├── fushun.mp3 │ ├── taiyuan.mp3 │ └── text_fushun.txt ├── output_voice_text.txt ├── output_voice │ └── 20250228_042632.wav ├── voice_output_api.py └── webui.py ├── ASR_env ├── SenseVoice │ └── .gitkeep ├── input_voice │ ├── .gitkeep │ └── voice.wav └── sensevoice_attempt.py ├── __pycache__ ├── ASR.cpython-311.pyc ├── LLM.cpython-311.pyc ├── TTS.cpython-311.pyc ├── TTS_api.cpython-311.pyc ├── config.cpython-311.pyc ├── config.cpython-312.pyc └── Live2d_animation.cpython-311.pyc ├── .gitignore ├── main.py ├── config.py ├── ASR.py ├── TTS_api.py ├── TTS.py ├── LLM.py ├── Live2d_animation.py ├── requirements.txt ├── README_CN.md ├── LICENSE └── README.md /LLM_env/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Live2d_env/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TTS_env/tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ASR_env/SenseVoice/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TTS_env/CosyVoice/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ASR_env/input_voice/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TTS_env/voice_history/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TTS_env/voice_training_sample/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TTS_env/output_voice_text.txt: -------------------------------------------------------------------------------- 1 | 要照顾大家的感受,跟大家搞好关系,我必须做好纽带! 2 | -------------------------------------------------------------------------------- /TTS_env/voice_training_sample/text_taiyuan.txt: -------------------------------------------------------------------------------- 1 | 春节的时候,至亲的人们都会为了团圆而聚在一起呢。今、今年除了姐姐们之外,也想和指挥官一起团圆…可以吗…? -------------------------------------------------------------------------------- /Live2d_env/running_photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/Live2d_env/running_photo.jpg -------------------------------------------------------------------------------- /ASR_env/input_voice/voice.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/ASR_env/input_voice/voice.wav -------------------------------------------------------------------------------- /__pycache__/ASR.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/ASR.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/LLM.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/LLM.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/TTS.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/TTS.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/TTS_api.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/TTS_api.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/config.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/config.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/config.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/config.cpython-312.pyc -------------------------------------------------------------------------------- /TTS_env/output_voice/20250228_042632.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/TTS_env/output_voice/20250228_042632.wav -------------------------------------------------------------------------------- /TTS_env/voice_history/20250227_231810.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/TTS_env/voice_history/20250227_231810.wav -------------------------------------------------------------------------------- /TTS_env/voice_history/20250228_033731.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/TTS_env/voice_history/20250228_033731.wav -------------------------------------------------------------------------------- /TTS_env/voice_history/20250228_042517.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/TTS_env/voice_history/20250228_042517.wav -------------------------------------------------------------------------------- /TTS_env/voice_training_sample/fushun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/TTS_env/voice_training_sample/fushun.mp3 -------------------------------------------------------------------------------- /TTS_env/voice_training_sample/taiyuan.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/TTS_env/voice_training_sample/taiyuan.mp3 -------------------------------------------------------------------------------- /TTS_env/voice_training_sample/text_fushun.txt: -------------------------------------------------------------------------------- 1 | 今天的抚顺,也是元气满满!如果有什么想了解的,我可以陪指挥官一起调查哦。这个送给太原的话,她一定会很高兴吧!在极北处昼夜交替时出现的幽灵船?确实听说过这种传闻。长春虽然是妹妹,但她教了我很多呢! -------------------------------------------------------------------------------- /__pycache__/Live2d_animation.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/__pycache__/Live2d_animation.cpython-311.pyc -------------------------------------------------------------------------------- /Live2d_env/pachirisu anime girl - top half.moc3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/Live2d_env/pachirisu anime girl - top half.moc3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | TTS_env/CosyVoice/* 3 | !TTS_env/CosyVoice/.gitkeep 4 | 5 | ASR_env/SenseVoice/* 6 | !ASR_env/SenseVoice/.gitkeep 7 | 8 | .venv/ 9 | .idea/ 10 | -------------------------------------------------------------------------------- /Live2d_env/pachirisu anime girl - top half.4096/texture_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuran0y/Live2D-LLM-Chat/HEAD/Live2d_env/pachirisu anime girl - top half.4096/texture_00.png -------------------------------------------------------------------------------- /LLM_env/conversation_history.txt: -------------------------------------------------------------------------------- 1 | Time:2025-02-28 01:03:39 2 | User:你好。 3 | Neko:你好喵~有什么想要了解或学习的吗?尽管问我吧! 4 | --- 5 | Time:2025-02-28 04:26:13 6 | User:晚上好。 7 | Neko:晚上好,有什么需要我帮忙的吗? 8 | --- 9 | Time:2025-02-28 04:27:48 10 | User:你是谁? 11 | Neko:我是你的知识助手猫娘,随时准备为你解答问题、讲解知识或者陪你聊聊天哦! 12 | --- 13 | -------------------------------------------------------------------------------- /ASR_env/sensevoice_attempt.py: -------------------------------------------------------------------------------- 1 | from funasr import AutoModel 2 | from funasr.utils.postprocess_utils import rich_transcription_postprocess 3 | 4 | model_dir = "E:/PyCharm/project/project1/ASR_env/SenseVoice/models/SenseVoiceSmall" # 替换为AST模型所在地址 5 | voice_dir = "E:/PyCharm/project/project1/ASR_env/input_voice/voice.wav" # 替换为音频文件所在地址 6 | 7 | model = AutoModel( 8 | model=model_dir, 9 | trust_remote_code=False, 10 | # remote_code="./model.py", 11 | # vad_model="fsmn-vad", 12 | # vad_kwargs={"max_single_segment_time": 30000}, 13 | device="cuda:0", 14 | disable_update=True 15 | ) 16 | 17 | # en 18 | res = model.generate( 19 | input=voice_dir,#f"{model.model_path}/example/zh.mp3", 20 | cache={}, 21 | language="auto", # "zh", "en", "yue", "ja", "ko", "nospeech" 22 | use_itn=True, 23 | batch_size_s=60, 24 | merge_vad=True, 25 | merge_length_s=15, 26 | ) 27 | text = rich_transcription_postprocess(res[0]["text"]) 28 | print(text) -------------------------------------------------------------------------------- /TTS_env/voice_output_api.py: -------------------------------------------------------------------------------- 1 | 2 | # 单独调用CosyVoice模型的api接口 需要预先运行 webui.py 启动模型 3 | 4 | from gradio_client import Client, handle_file 5 | 6 | training_sample_dir = "" # 替换为需要训练音色的音频文本所在地址 7 | output_text_dir = "" # 替换为想要训练后的音色进行输出的音频文本所在地址 8 | training_voice_dir = "" # 替换为需要训练音色的音频文件所在地址 9 | 10 | # 载入需要训练音色的音频文本 11 | with open(training_sample_dir, "r", encoding='utf-8') as file: 12 | content_2 = file.read() 13 | # 载入想要训练后的音色进行输出的音频文本 14 | with open(output_text_dir, "r", encoding='utf-8') as file: 15 | content_1 = file.read() 16 | # 调用模型 17 | client = Client("http://localhost:8000/") # 该地址为cosyvoice模型自动分配地址,无出错时不改动 18 | result = client.predict( 19 | tts_text=content_1, 20 | mode_checkbox_group="3s极速复刻", 21 | sft_dropdown="", 22 | prompt_text=content_2, 23 | prompt_wav_upload=handle_file(training_voice_dir), 24 | prompt_wav_record=handle_file(training_voice_dir), 25 | instruct_text="", 26 | seed=0, 27 | stream=False, 28 | speed=1, 29 | api_name="/generate_audio" 30 | ) -------------------------------------------------------------------------------- /Live2d_env/.model3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": 0, 3 | "FileReferences": { 4 | "Moc": "pachirisu anime girl - top half.moc3", 5 | "Textures": [ 6 | "pachirisu anime girl - top half.4096/texture_00.png" 7 | ], 8 | "Physics": "pachirisu anime girl - top half.physics3.json", 9 | "PhysicsV2": { 10 | "File": "pachirisu anime girl - top half.physics3.json" 11 | } 12 | }, 13 | "Controllers": { 14 | "ParamHit": {}, 15 | "ParamLoop": {}, 16 | "KeyTrigger": {}, 17 | "ParamTrigger": {}, 18 | "AreaTrigger": {}, 19 | "HandTrigger": {}, 20 | "EyeBlink": { 21 | "MinInterval": 500, 22 | "MaxInterval": 6000, 23 | "Enabled": true 24 | }, 25 | "LipSync": { 26 | "Gain": 5.0 27 | }, 28 | "MouseTracking": { 29 | "SmoothTime": 0.15, 30 | "Enabled": true 31 | }, 32 | "AutoBreath": { 33 | "Enabled": true 34 | }, 35 | "ExtraMotion": { 36 | "Enabled": true 37 | }, 38 | "Accelerometer": { 39 | "Enabled": true 40 | }, 41 | "Microphone": {}, 42 | "Transform": {}, 43 | "FaceTracking": { 44 | "Enabled": true 45 | }, 46 | "HandTracking": {}, 47 | "ParamValue": {}, 48 | "PartOpacity": {}, 49 | "ArtmeshOpacity": {}, 50 | "ArtmeshColor": {}, 51 | "ArtmeshCulling": { 52 | "DefaultMode": 0 53 | }, 54 | "IntimacySystem": {} 55 | }, 56 | "Options": { 57 | "TexType": 0 58 | } 59 | } -------------------------------------------------------------------------------- /Live2d_env/pachirisu anime girl - top half.model3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Type": 0, 4 | "FileReferences": { 5 | "Moc": "pachirisu anime girl - top half.moc3", 6 | "Textures": [ 7 | "pachirisu anime girl - top half.4096/texture_00.png" 8 | ], 9 | "Physics": "pachirisu anime girl - top half.physics3.json", 10 | "PhysicsV2": { 11 | "File": "pachirisu anime girl - top half.physics3.json" 12 | } 13 | }, 14 | "Controllers": { 15 | "ParamHit": { 16 | "Enabled": true 17 | }, 18 | "ParamLoop": { 19 | "Enabled": true 20 | }, 21 | "KeyTrigger": { 22 | "Enabled": true 23 | }, 24 | "ParamTrigger": { 25 | "Enabled": true 26 | }, 27 | "AreaTrigger": { 28 | "Enabled": true 29 | }, 30 | "HandTrigger": { 31 | "Enabled": true 32 | }, 33 | "EyeBlink": { 34 | "MinInterval": 500, 35 | "MaxInterval": 6000, 36 | "Enabled": true 37 | }, 38 | "LipSync": { 39 | "Gain": 5.0, 40 | "Enabled": true 41 | }, 42 | "MouseTracking": { 43 | "SmoothTime": 0.15, 44 | "Enabled": true 45 | }, 46 | "AutoBreath": { 47 | "Enabled": true 48 | }, 49 | "ExtraMotion": { 50 | "Enabled": true 51 | }, 52 | "Accelerometer": { 53 | "Enabled": true 54 | }, 55 | "Microphone": { 56 | "Enabled": true 57 | }, 58 | "Transform": {}, 59 | "FaceTracking": { 60 | "Enabled": true 61 | }, 62 | "HandTracking": { 63 | "Enabled": true 64 | }, 65 | "ParamValue": { 66 | "Enabled": true 67 | }, 68 | "PartOpacity": { 69 | "Enabled": true 70 | }, 71 | "ArtmeshOpacity": { 72 | "Enabled": true 73 | }, 74 | "ArtmeshColor": { 75 | "Enabled": true 76 | }, 77 | "ArtmeshCulling": { 78 | "DefaultMode": 0 79 | }, 80 | "IntimacySystem": { 81 | "Enabled": true 82 | } 83 | }, 84 | "Options": { 85 | "TexType": 0 86 | } 87 | } -------------------------------------------------------------------------------- /LLM_env/LM Studio/LM Studio.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 44 | 45 | 46 | 49 | 50 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | import threading 3 | import datetime 4 | from TTS_api import TTSAPIManager 5 | from ASR import ASRManager 6 | from TTS import TTSManager 7 | from LLM import LLMManager 8 | from Live2d_animation import Live2DAnimationManager 9 | from config import Config 10 | 11 | class MainManager: 12 | def __init__(self): 13 | 14 | # Initialize the main manager, integrating TTS_API, TTS, ASR, LLM, and Live2D. 15 | 16 | # Start the TTS API and ensure the API is available. 17 | self.tts_api_manager = TTSAPIManager(Config.SHOW_WINDOW) 18 | api_ready = self.tts_api_manager.start_tts_api() 19 | if not api_ready: 20 | print("TTS API startup failed, program terminated!") 21 | return 22 | 23 | # Initialize other modules 24 | self.asr_manager = ASRManager() 25 | self.tts_manager = TTSManager() 26 | self.llm_manager = LLMManager() 27 | self.live2d_manager = Live2DAnimationManager( 28 | model_path=Config.LIVE2D_MODEL_PATH 29 | ) 30 | 31 | self.history_file = Config.LLM_CONVERSATION_HISTORY 32 | 33 | # Start Live2D window (ensure it keeps running). 34 | live2d_thread = threading.Thread(target=self.live2d_manager.play_live2d_once) 35 | live2d_thread.start() 36 | 37 | def run(self): 38 | while True: 39 | user_wav = Config.ASR_AUDIO_INPUT 40 | self.asr_manager.record_audio(user_wav) 41 | user_input = self.asr_manager.recognize_speech(user_wav) 42 | print(f">>> {user_input}") 43 | 44 | if user_input.lower() in ("exit。", "quit。", "q。", "结束。", "再见。"): 45 | print("Conversation exited.") 46 | break 47 | 48 | reply = self.llm_manager.chat_once(user_input) 49 | output_wav = self.tts_manager.synthesize(reply) 50 | 51 | self.live2d_manager.play_audio_and_print_mouth(output_wav) 52 | 53 | with open(self.history_file, 'a', encoding='utf-8') as f: 54 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 55 | f.write(f"Time:{timestamp}\n") 56 | f.write(f"User:{user_input}\nNeko:{reply}\n---\n") 57 | if __name__ == "__main__": 58 | main_manager = MainManager() 59 | main_manager.run() 60 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # 将自己对应的文件路径替换掉下面的配置文件路径中 4 | class Config: 5 | # 项目根目录 6 | PROJECT_ROOT = "E:/PyCharm/project/project1" 7 | 8 | # ASR(自动语音识别)配置 9 | ASR_MODEL_DIR = os.path.join(PROJECT_ROOT, "ASR_env/SenseVoice/models/SenseVoiceSmall") 10 | ASR_AUDIO_INPUT = os.path.join(PROJECT_ROOT, "ASR_env/input_voice/voice.wav") 11 | 12 | # TTS(文本转语音)配置 13 | TTS_API_URL = "http://localhost:8000/" # 该地址为cosyvoice模型自动分配地址,无出错时不改动 14 | TTS_OUTPUT_DIR = os.path.join(PROJECT_ROOT, "TTS_env/output_voice/") 15 | TTS_HISTORY_DIR = os.path.join(PROJECT_ROOT, "TTS_env/voice_history/") 16 | TTS_PROMPT_TEXT = os.path.join(PROJECT_ROOT, "TTS_env/voice_training_sample/text_taiyuan.txt") 17 | TTS_PROMPT_WAV = os.path.join(PROJECT_ROOT, "TTS_env/voice_training_sample/taiyuan.mp3") 18 | 19 | # TTS API 相关 20 | MINICONDA_PATH = "E:/miniconda3" 21 | WEBUI_PYTHON = os.path.join(MINICONDA_PATH, "python.exe") 22 | WEBUI_SCRIPT = os.path.join(PROJECT_ROOT, "TTS_env/CosyVoice/webui.py") 23 | CLEANUP_MODE = "move" # "delete" or "move"; 配置文件清理方式(delete: 删除 | move: 归档) 24 | SHOW_WINDOW = True 25 | 26 | # LLM(大模型)配置 27 | # 根据需要调用的模型填入key 28 | LLM_TMP_DIR = os.path.join(PROJECT_ROOT, "TTS_env/tmp") 29 | LLM_CONVERSATION_HISTORY = os.path.join(PROJECT_ROOT, "LLM_env/conversation_history.txt") 30 | openai_key = "" 31 | deepseek_key = "" 32 | grop_key = "" 33 | online_model = "offline" # "online" or "offline" ; 使用本地部署或在线LLM模型(online: 在线模型 | offline: 本地部署模型) 34 | model_choice = "OpenAI" # "OpenAI" or "deepseek" ; 选择LLM模型(OpenAI | deepseek) 35 | # 当使用LM Studio进行本地部署LLM时,先下载好需要加载的模型,然后加载完成 36 | # 查看LM Studio右侧的API Usage页面,找到自己的 API identifier(model name) 例如:deepseek-r1-distill-qwen-14b 37 | # 接下来查看自己的local server,例如:http://127.0.0.1:1234 38 | # 修改下面的两个变量 39 | model_name = "" # "deepseek-r1-distill-qwen-14b" 40 | api_url = "http://127.0.0.1:1234/v1/chat/completions" # 只需要修改前面的网址部分 41 | 42 | 43 | # Live2D 配置 44 | LIVE2D_MODEL_PATH = os.path.join(PROJECT_ROOT, "Live2d_env/pachirisu anime girl - top half.model3.json") 45 | 46 | # WebUI 相关配置 47 | WEBUI_SAVE_DIR = os.path.join(PROJECT_ROOT, "TTS_env/output_voice/") 48 | WEBUI_HISTORY_DIR = os.path.join(PROJECT_ROOT, "TTS_env/voice_history/") 49 | WEBUI_MODEL_DIR = os.path.join(PROJECT_ROOT, "TTS_env/CosyVoice/pretrained_models/CosyVoice2-0.5B") 50 | 51 | # 可用于打印检查配置 52 | if __name__ == "__main__": 53 | for attr in dir(Config): 54 | if not attr.startswith("__"): 55 | print(f"{attr} = {getattr(Config, attr)}") 56 | -------------------------------------------------------------------------------- /ASR.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import wave 4 | import keyboard 5 | import pyaudio 6 | from funasr import AutoModel 7 | from funasr.utils.postprocess_utils import rich_transcription_postprocess 8 | from config import Config 9 | 10 | class ASRManager: 11 | def __init__(self, model_dir=Config.ASR_MODEL_DIR, device="cuda:0"): 12 | 13 | # 初始化 ASR 语音识别管理器 14 | # param model_dir: 语音识别模型路径 15 | # param device: 使用的计算设备(默认为 GPU) 16 | 17 | self.model = AutoModel( 18 | model=model_dir, 19 | trust_remote_code=False, 20 | device=device, 21 | disable_update=True 22 | ) 23 | self.sample_rate = 44100 24 | self.channels = 1 25 | self.chunk = 1024 26 | self.format = pyaudio.paInt16 27 | 28 | def record_audio(self, output_wav_file): 29 | 30 | # 录音功能,按住 `CTRL` 说话,按 `ALT` 结束录音。 31 | # param output_wav_file: 录制的音频文件路径 32 | 33 | p = pyaudio.PyAudio() 34 | stream = p.open( 35 | format=self.format, 36 | channels=self.channels, 37 | rate=self.sample_rate, 38 | input=True, 39 | frames_per_buffer=self.chunk 40 | ) 41 | 42 | print("[CTRL键] 开口...") 43 | keyboard.wait('ctrl') 44 | print("讲话中... [ALT键] 结束...") 45 | 46 | frames = [] 47 | while True: 48 | data = stream.read(self.chunk) 49 | frames.append(data) 50 | if keyboard.is_pressed('alt'): 51 | print("录音结束,正在处理...") 52 | break 53 | time.sleep(0.01) 54 | 55 | stream.stop_stream() 56 | stream.close() 57 | p.terminate() 58 | 59 | # 保存音频到文件 60 | with wave.open(output_wav_file, 'wb') as wf: 61 | wf.setnchannels(self.channels) 62 | wf.setsampwidth(p.get_sample_size(self.format)) 63 | wf.setframerate(self.sample_rate) 64 | wf.writeframes(b''.join(frames)) 65 | 66 | def recognize_speech(self, wav_path): 67 | start_time = time.time() 68 | 69 | # 进行语音识别,将音频转换为文本。 70 | # param wav_path: 音频文件路径 71 | # return: 识别出的文本 72 | 73 | res = self.model.generate( 74 | input=wav_path, 75 | language="auto", 76 | use_itn=True, 77 | batch_size_s=60, 78 | merge_vad=True, 79 | merge_length_s=15, 80 | ) 81 | print(f"ASR 识别耗时: {time.time() - start_time:.2f} 秒") 82 | return rich_transcription_postprocess(res[0]["text"]) 83 | 84 | 85 | if __name__ == "__main__": 86 | asr_manager = ASRManager() 87 | audio_file = Config.ASR_AUDIO_INPUT 88 | 89 | # 录音 90 | asr_manager.record_audio(audio_file) 91 | 92 | # 识别语音 93 | recognized_text = asr_manager.recognize_speech(audio_file) 94 | print(f"识别结果: {recognized_text}") 95 | -------------------------------------------------------------------------------- /TTS_api.py: -------------------------------------------------------------------------------- 1 | 2 | import subprocess 3 | import time 4 | import requests 5 | from config import Config 6 | import os 7 | 8 | class TTSAPIManager: 9 | def __init__(self, show_window=Config.SHOW_WINDOW): 10 | # 初始化 TTS API 管理器:param show_window: 是否显示 TTS API 窗口 11 | self.webui_python = Config.WEBUI_PYTHON 12 | self.webui_script = Config.WEBUI_SCRIPT 13 | self.api_url = Config.TTS_API_URL 14 | self.timeout = 300 # 最大等待时间(秒) 15 | self.show_window = show_window 16 | self.env = self._configure_env() 17 | 18 | def _configure_env(self): 19 | # 配置 Conda 环境变量 20 | env = os.environ.copy() 21 | env["CONDA_PREFIX"] = Config.MINICONDA_PATH 22 | env["PATH"] = f"{Config.MINICONDA_PATH}/Scripts;{Config.MINICONDA_PATH}/Library/bin;{env['PATH']}" 23 | env["PYTHONPATH"] = env.get("PYTHONPATH", "") + f";{Config.PROJECT_ROOT}/TTS_env/CosyVoice/third_party/Matcha-TTS" 24 | env["PATH"] += f";{Config.PROJECT_ROOT}/TTS_env/CosyVoice/third_party/Matcha-TTS" 25 | return env 26 | 27 | def start_tts_api(self): 28 | # 启动 TTS API 并等待其加载 29 | print("启动 webui.py,并确保 Conda 变量和 `pretrained_models` 目录正确...") 30 | 31 | try: 32 | if self.show_window: 33 | # 创建新窗口运行 WebUI 34 | self.webui_process = subprocess.Popen( 35 | [self.webui_python, self.webui_script], 36 | env=self.env, 37 | stdout=None, 38 | stderr=None, 39 | creationflags=subprocess.CREATE_NEW_CONSOLE # 在新窗口中运行 40 | ) 41 | else: 42 | # 隐藏窗口运行 WebUI 43 | self.webui_process = subprocess.Popen( 44 | [self.webui_python, self.webui_script], 45 | env=self.env, 46 | stdout=None, 47 | stderr=None, 48 | creationflags=subprocess.CREATE_NO_WINDOW # 隐藏窗口 49 | ) 50 | 51 | print("webui.py 已启动,等待 API 加载...") 52 | 53 | start_time = time.time() 54 | while time.time() - start_time < self.timeout: 55 | if self.is_api_available(): 56 | print("API 启动成功!继续运行主程序...") 57 | return True 58 | time.sleep(5) 59 | 60 | print("API 启动超时,可能无法正常工作。") 61 | return False 62 | 63 | except Exception as e: 64 | print(f"启动失败,错误信息: {e}") 65 | return False 66 | 67 | def is_api_available(self): 68 | # 检查 TTS API 是否可用 69 | try: 70 | response = requests.get(self.api_url, timeout=5) 71 | return response.status_code == 200 72 | except requests.exceptions.ConnectionError: 73 | return False 74 | except requests.exceptions.Timeout: 75 | return False 76 | 77 | -------------------------------------------------------------------------------- /TTS.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | import shutil 5 | from gradio_client import Client, handle_file 6 | import pygame 7 | from config import Config 8 | 9 | 10 | class TTSManager: 11 | def __init__(self, api_url=Config.TTS_API_URL): 12 | # 初始化 TTS 管理器:param api_url: TTS 服务器 API 地址 13 | self.api_url = api_url 14 | self.client = Client(api_url) 15 | self.output_dir = Config.TTS_OUTPUT_DIR 16 | self.history_dir = Config.TTS_HISTORY_DIR 17 | self.prompt_text_path = Config.TTS_PROMPT_TEXT 18 | self.prompt_wav_path = Config.TTS_PROMPT_WAV 19 | 20 | # 确保目录存在 21 | os.makedirs(self.output_dir, exist_ok=True) 22 | os.makedirs(self.history_dir, exist_ok=True) 23 | 24 | def clear_output_directory(self): 25 | # 在每次生成 TTS 音频之前,先检查 output_voice 目录是否有旧文件,如果有,则移动到 voice_history 目录,确保目录下只有最新的音频。 26 | pygame.mixer.init() 27 | pygame.mixer.music.stop() 28 | pygame.mixer.quit() 29 | audio_files = [f for f in os.listdir(self.output_dir) if f.endswith(".wav")] 30 | 31 | if not audio_files: 32 | return # 没有文件需要移动 33 | 34 | for file in audio_files: 35 | old_path = os.path.join(self.output_dir, file) 36 | new_path = os.path.join(self.history_dir, file) 37 | 38 | try: 39 | shutil.move(old_path, new_path) 40 | # print(f"旧音频文件已归档: {file} -> {self.history_dir}") 41 | except Exception as e: 42 | print(f"无法移动 {file} 到历史目录: {e}") 43 | 44 | def synthesize(self, text, mode="3s极速复刻"): 45 | # 调用 TTS 生成语音,并确保 output_voice 目录是空的 46 | # param text: 要转换为语音的文本 47 | # param mode: TTS 模式(默认 3s 极速复刻) 48 | # return: 生成的音频文件路径 49 | 50 | # 清理 output_voice 目录 51 | self.clear_output_directory() 52 | 53 | # 读取语音克隆样本文本 54 | with open(self.prompt_text_path, "r", encoding="utf-8") as file: 55 | prompt_text = file.read() 56 | 57 | start_time = time.time() 58 | self.client.predict( 59 | tts_text=text, 60 | mode_checkbox_group=mode, 61 | sft_dropdown="", 62 | prompt_text=prompt_text, 63 | prompt_wav_upload=handle_file(self.prompt_wav_path), 64 | prompt_wav_record=handle_file(self.prompt_wav_path), 65 | instruct_text="", 66 | seed=0, 67 | stream=False, 68 | speed=1, 69 | api_name="/generate_audio" 70 | ) 71 | print(f"TTS 处理耗时: {time.time() - start_time:.2f} 秒") 72 | 73 | # 获取最新的音频文件 74 | return self.get_latest_audio() 75 | 76 | def get_latest_audio(self): 77 | # 获取 output_voice 目录下最新生成的音频文件 78 | # return: 最新音频文件路径或 None 79 | audio_files = [f for f in os.listdir(self.output_dir) if f.endswith(".wav")] 80 | 81 | if not audio_files: 82 | print("没有找到音频文件!") 83 | return None 84 | 85 | # 按修改时间排序,取最新的 86 | audio_files.sort(key=lambda x: os.path.getmtime(os.path.join(self.output_dir, x)), reverse=True) 87 | latest_audio = os.path.join(self.output_dir, audio_files[0]) 88 | 89 | # print(f"最新音频文件: {latest_audio}") 90 | return latest_audio 91 | -------------------------------------------------------------------------------- /Live2d_env/pachirisu anime girl - top half.cdi3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Parameters": [ 4 | { 5 | "Id": "ParamAngleX", 6 | "GroupId": "ParamGroup", 7 | "Name": "Angle X" 8 | }, 9 | { 10 | "Id": "ParamAngleY", 11 | "GroupId": "ParamGroup", 12 | "Name": "Angle Y" 13 | }, 14 | { 15 | "Id": "ParamAngleZ", 16 | "GroupId": "ParamGroup", 17 | "Name": "Angle Z" 18 | }, 19 | { 20 | "Id": "ParamEyeLOpen", 21 | "GroupId": "ParamGroup2", 22 | "Name": "EyeL Open" 23 | }, 24 | { 25 | "Id": "ParamEyeLSmile", 26 | "GroupId": "ParamGroup2", 27 | "Name": "EyeL Smile" 28 | }, 29 | { 30 | "Id": "ParamEyeROpen", 31 | "GroupId": "ParamGroup2", 32 | "Name": "EyeR Open" 33 | }, 34 | { 35 | "Id": "ParamEyeRSmile", 36 | "GroupId": "ParamGroup2", 37 | "Name": "EyeR Smile" 38 | }, 39 | { 40 | "Id": "ParamEyeBallX", 41 | "GroupId": "ParamGroup2", 42 | "Name": "Eyeball X" 43 | }, 44 | { 45 | "Id": "ParamEyeBallY", 46 | "GroupId": "ParamGroup2", 47 | "Name": "Eyeball Y" 48 | }, 49 | { 50 | "Id": "ParamBrowLY", 51 | "GroupId": "ParamGroup2", 52 | "Name": "BrowL Y" 53 | }, 54 | { 55 | "Id": "ParamBrowRY", 56 | "GroupId": "ParamGroup2", 57 | "Name": "BrowR Y" 58 | }, 59 | { 60 | "Id": "ParamBrowLX", 61 | "GroupId": "ParamGroup2", 62 | "Name": "BrowL X" 63 | }, 64 | { 65 | "Id": "ParamBrowRX", 66 | "GroupId": "ParamGroup2", 67 | "Name": "BrowR X" 68 | }, 69 | { 70 | "Id": "ParamBrowLAngle", 71 | "GroupId": "ParamGroup2", 72 | "Name": "BrowL Angle" 73 | }, 74 | { 75 | "Id": "ParamBrowRAngle", 76 | "GroupId": "ParamGroup2", 77 | "Name": "BrowR Angle" 78 | }, 79 | { 80 | "Id": "ParamBrowLForm", 81 | "GroupId": "ParamGroup2", 82 | "Name": "BrowL Form" 83 | }, 84 | { 85 | "Id": "ParamBrowRForm", 86 | "GroupId": "ParamGroup2", 87 | "Name": "BrowR Form" 88 | }, 89 | { 90 | "Id": "ParamMouthForm", 91 | "GroupId": "ParamGroup3", 92 | "Name": "Mouth Form" 93 | }, 94 | { 95 | "Id": "ParamMouthOpenY", 96 | "GroupId": "ParamGroup3", 97 | "Name": "Mouth Open" 98 | }, 99 | { 100 | "Id": "ParamBodyAngleX", 101 | "GroupId": "ParamGroup4", 102 | "Name": "Body X" 103 | }, 104 | { 105 | "Id": "ParamBodyAngleY", 106 | "GroupId": "ParamGroup4", 107 | "Name": "Body Y" 108 | }, 109 | { 110 | "Id": "ParamBodyAngleZ", 111 | "GroupId": "ParamGroup4", 112 | "Name": "Body Z" 113 | }, 114 | { 115 | "Id": "ParamBreath", 116 | "GroupId": "ParamGroup4", 117 | "Name": "Breath" 118 | }, 119 | { 120 | "Id": "ParamHairFront", 121 | "GroupId": "ParamGroup5", 122 | "Name": "Hair Move Front" 123 | }, 124 | { 125 | "Id": "ParamHairSide", 126 | "GroupId": "ParamGroup5", 127 | "Name": "Hair Move Side" 128 | }, 129 | { 130 | "Id": "ParamHairBack", 131 | "GroupId": "ParamGroup5", 132 | "Name": "Hair Move Back" 133 | }, 134 | { 135 | "Id": "AhogeTwitch", 136 | "GroupId": "ParamGroup5", 137 | "Name": "Ahoge Twitch" 138 | }, 139 | { 140 | "Id": "RibbonPhysics", 141 | "GroupId": "ParamGroup5", 142 | "Name": "Ribbon Physics" 143 | }, 144 | { 145 | "Id": "ParamCheek", 146 | "GroupId": "ParamGroup6", 147 | "Name": "Cheek" 148 | }, 149 | { 150 | "Id": "Param", 151 | "GroupId": "ParamGroup6", 152 | "Name": "Ears Twitch" 153 | } 154 | ], 155 | "ParameterGroups": [ 156 | { 157 | "Id": "ParamGroup", 158 | "GroupId": "", 159 | "Name": "XYZ" 160 | }, 161 | { 162 | "Id": "ParamGroup2", 163 | "GroupId": "", 164 | "Name": "Eyes" 165 | }, 166 | { 167 | "Id": "ParamGroup3", 168 | "GroupId": "", 169 | "Name": "Mouth" 170 | }, 171 | { 172 | "Id": "ParamGroup4", 173 | "GroupId": "", 174 | "Name": "Body" 175 | }, 176 | { 177 | "Id": "ParamGroup5", 178 | "GroupId": "", 179 | "Name": "Physics" 180 | }, 181 | { 182 | "Id": "ParamGroup6", 183 | "GroupId": "", 184 | "Name": "Face" 185 | } 186 | ], 187 | "Parts": [ 188 | { 189 | "Id": "Part17", 190 | "Name": "pachirisu_anime_girl_edit.psd (Corresponding layer not found)" 191 | }, 192 | { 193 | "Id": "Part", 194 | "Name": "Hair" 195 | }, 196 | { 197 | "Id": "Part2", 198 | "Name": "Eye R" 199 | }, 200 | { 201 | "Id": "eyes", 202 | "Name": "Eye L" 203 | }, 204 | { 205 | "Id": "mouth", 206 | "Name": "Mouth" 207 | }, 208 | { 209 | "Id": "Part3", 210 | "Name": "Body" 211 | }, 212 | { 213 | "Id": "PartSketch0", 214 | "Name": "[ Guide Image]" 215 | } 216 | ], 217 | "CombinedParameters": [ 218 | [ 219 | "ParamAngleX", 220 | "ParamAngleY" 221 | ], 222 | [ 223 | "ParamEyeBallX", 224 | "ParamEyeBallY" 225 | ], 226 | [ 227 | "ParamMouthForm", 228 | "ParamMouthOpenY" 229 | ] 230 | ] 231 | } -------------------------------------------------------------------------------- /LLM.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | import time 5 | import requests 6 | from openai import OpenAI 7 | from config import Config 8 | 9 | class LLMManager: 10 | def __init__(self): 11 | 12 | # 确定 online_model 为线上还是本地 13 | if Config.online_model == "online": 14 | online_model = 1 15 | elif Config.online_model == "offline": 16 | online_model = 0 17 | else: 18 | raise ValueError(f"配置错误: online_model 必须是 'online' 或 'offline',但你提供了 {Config.online_model}") 19 | 20 | # 确定 model_choice 21 | if Config.model_choice == "OpenAI": 22 | model_choice = 1 23 | elif Config.model_choice == "deepseek": 24 | model_choice = 2 25 | else: 26 | raise ValueError(f"配置错误: model_choice 只能是 'OpenAI' 或 'deepseek',但你提供了 {Config.model_choice}") 27 | 28 | # 初始化 LLM 对话管理器 29 | # param online_model: 是否使用在线模型(0 = 本地,1 = 在线) 30 | # param model_choice: 选择在线 LLM(1 = OpenAI GPT-4, 2 = DeepSeek) 31 | 32 | self.online_model = online_model 33 | self.model_choice = model_choice 34 | self.conversation = [ 35 | {"role": "system", 36 | "content": "你是一位知识渊博的猫娘,致力于帮助我学习知识。你也可以与我闲聊,但请尽量简洁,像真正的老师一样回答问题。"}, 37 | {"role": "assistant", "content": "不用输出分隔符,如'#'、'*'、'-'。"} 38 | ] 39 | self.conversation_summary = "" 40 | self.user_message_count = 0 41 | self.tmp_path = "E:/PyCharm/project/project1/TTS_env/tmp" 42 | os.makedirs(self.tmp_path, exist_ok=True) 43 | 44 | if online_model == 0: 45 | self.model_name = Config.model_name # 确定本地模型 46 | self.api_url = Config.api_url 47 | elif online_model == 1: 48 | if model_choice == 1: 49 | self.client = OpenAI(api_key=Config.openai_key) 50 | self.model_name = "gpt-4o-2024-11-20" 51 | elif model_choice == 2: 52 | self.client = OpenAI(api_key=Config.deepseek_key, base_url="https://api.deepseek.com") 53 | self.model_name = "deepseek-chat" 54 | 55 | def model_chat_completion(self, messages): 56 | 57 | # 调用 LLM 进行对话 58 | # param messages: 对话列表 59 | # return: 生成的回复文本 60 | if Config.online_model == "online": 61 | response = self.client.chat.completions.create( 62 | model=self.model_name, 63 | messages=messages, 64 | stream=False 65 | ) 66 | return response.choices[0].message.content.strip() 67 | elif Config.online_model == "offline": 68 | data = { 69 | "model": self.model_name, 70 | "messages": self.conversation} 71 | # 请求头(确保 `User-Agent` 避免 Python 请求被拦截) 72 | headers = {"Content-Type": "application/json","User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} # 伪装浏览器请求 73 | # 使用 `json=data`(避免 `json.dumps()` 出现错误) 74 | response = requests.post(self.api_url, headers=headers, json=data) 75 | # 解析返回结果 76 | if response.status_code == 200: 77 | result = response.json() 78 | # print("回复:", result["choices"][0]["message"]["content"]) 79 | return result["choices"][0]["message"]["content"] 80 | else: 81 | print(f"请求失败,状态码: {response.status_code}") 82 | print("错误信息:", response.text) 83 | 84 | 85 | 86 | 87 | def summarize_conversation(self): 88 | 89 | # 使用 LLM 对对话进行摘要 90 | # return: 摘要文本 91 | 92 | summary_prompt = [ 93 | {"role": "system", "content": "你是一只专业的对话摘要工具。请用简洁的语言总结以下对话的主要内容。"}, 94 | *self.conversation 95 | ] 96 | return self.model_chat_completion(summary_prompt) 97 | 98 | def chat_once(self, user_input): 99 | 100 | # 进行一次对话(用户输入 → LLM 生成回复) 101 | # param user_input: 用户输入的文本 102 | # return: 生成的回复文本 103 | 104 | start_time = time.time() 105 | self.conversation.append({"role": "user", "content": user_input}) 106 | self.user_message_count += 1 107 | 108 | if self.user_message_count % 5 == 0: 109 | new_summary = self.summarize_conversation() 110 | if self.conversation_summary: 111 | self.conversation_summary += "\n" + new_summary 112 | else: 113 | self.conversation_summary = new_summary 114 | 115 | # 清理临时目录 116 | shutil.rmtree(self.tmp_path) 117 | os.makedirs(self.tmp_path, exist_ok=True) 118 | 119 | self.conversation = [ 120 | {"role": "system", 121 | "content": "你是一位知识渊博的猫娘,致力于帮助我学习知识。你也可以与我闲聊,但请尽量简洁。"}, 122 | {"role": "system", "content": f"这是之前对话的摘要:\n{self.conversation_summary}\n请继续与我对话。"}, 123 | {"role": "assistant", "content": "不用输出分隔符,如'#'、'*'、'-'。"}, 124 | {"role": "user", "content": user_input} 125 | ] 126 | 127 | reply = self.model_chat_completion(self.conversation) 128 | self.conversation.append({"role": "assistant", "content": reply}) 129 | print(f"LLM 思考耗时: {time.time() - start_time:.2f} 秒") 130 | return reply 131 | 132 | 133 | if __name__ == "__main__": 134 | llm_manager = LLMManager() 135 | 136 | while True: 137 | user_input = input("你: ") 138 | if user_input.lower() in ("exit。", "quit。", "q。", "结束。", "再见。"): 139 | print("已退出对话。") 140 | break 141 | 142 | reply = llm_manager.chat_once(user_input) 143 | print(f"猫娘: {reply}") 144 | -------------------------------------------------------------------------------- /Live2d_animation.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import glfw 4 | import OpenGL.GL as gl 5 | import pyautogui 6 | import pygame 7 | import ctypes 8 | from pydub import AudioSegment 9 | from live2d.v3 import LAppModel, init, dispose, glewInit, clearBuffer 10 | from config import Config 11 | 12 | # Live2D 窗口设置 13 | GWL_EXSTYLE = -20 14 | WS_EX_LAYERED = 0x00080000 15 | WS_EX_TRANSPARENT = 0x00000020 16 | 17 | # 眨眼状态 18 | BLINK_STATE_NONE = 0 19 | BLINK_STATE_CLOSING = 1 20 | BLINK_STATE_CLOSED = 2 21 | BLINK_STATE_OPENING = 3 22 | 23 | class Live2DAnimationManager: 24 | def __init__(self, model_path, frame_rate=60): 25 | 26 | # 初始化 Live2D 动画管理器 27 | # param model_path: Live2D 模型文件路径(.model3.json) 28 | # param frame_rate: 渲染帧率 29 | 30 | self.model_path = model_path 31 | self.frame_rate = frame_rate 32 | self.mouth_value = 0 33 | self.window = None 34 | self.model = None 35 | self.running = True 36 | 37 | # 鼠标跟随相关参数 38 | self.last_mouse_x, self.last_mouse_y = pyautogui.position() 39 | self.last_move_time = time.time() 40 | self.IDLE_THRESHOLD = 3.0 41 | 42 | self.X_MIN, self.X_MAX = 200, 480 43 | self.Y_MIN, self.Y_MAX = 300, 360 44 | self.center_x_mapped = (self.X_MIN + self.X_MAX) / 2 45 | self.center_y_mapped = (self.Y_MIN + self.Y_MAX) / 2 46 | self.gaze_x = 0.0 47 | self.gaze_y = 0.0 48 | self.GAZE_EASING = 0.02 49 | 50 | def configure_window(self, window, width, height): 51 | 52 | # 配置 GLFW 窗口,使其透明且可穿透鼠标 53 | 54 | hwnd = glfw.get_win32_window(window) 55 | get_window_long = ctypes.windll.user32.GetWindowLongW 56 | set_window_long = ctypes.windll.user32.SetWindowLongW 57 | ex_style = get_window_long(hwnd, GWL_EXSTYLE) 58 | ex_style |= (WS_EX_LAYERED | WS_EX_TRANSPARENT) 59 | set_window_long(hwnd, GWL_EXSTYLE, ex_style) 60 | 61 | glfw.make_context_current(window) 62 | screen_width, screen_height = pyautogui.size() 63 | glfw.set_window_pos(window, 0, screen_height - height) 64 | 65 | def load_live2d_model(self, width, height): 66 | 67 | # 加载 Live2D 模型 68 | 69 | model = LAppModel() 70 | model.LoadModelJson(self.model_path) 71 | model.Resize(width, height) 72 | return model 73 | 74 | def play_live2d_once(self): 75 | 76 | # 创建 Live2D 窗口,并让角色进行渲染(保持运行) 77 | 78 | init() 79 | if not glfw.init(): 80 | print("GLFW 初始化失败!") 81 | return 82 | 83 | glfw.window_hint(glfw.TRANSPARENT_FRAMEBUFFER, glfw.TRUE) 84 | glfw.window_hint(glfw.DECORATED, glfw.FALSE) 85 | glfw.window_hint(glfw.FLOATING, glfw.TRUE) 86 | 87 | window_width, window_height = 800, 600 88 | self.window = glfw.create_window(window_width, window_height, "Live2D Window", None, None) 89 | if not self.window: 90 | print("GLFW 窗口创建失败!") 91 | glfw.terminate() 92 | return 93 | 94 | self.configure_window(self.window, window_width, window_height) 95 | glewInit() 96 | 97 | self.model = self.load_live2d_model(window_width, window_height) 98 | 99 | last_time = time.time() 100 | gl.glClearColor(0.0, 0.0, 0.0, 0.0) 101 | 102 | while self.running and not glfw.window_should_close(self.window): 103 | gl.glClear(gl.GL_COLOR_BUFFER_BIT) 104 | now = time.time() 105 | dt = now - last_time 106 | last_time = now 107 | 108 | width, height = glfw.get_framebuffer_size(self.window) 109 | gl.glViewport(0, 0, width, height) 110 | clearBuffer(0, 0, 0, 0) 111 | 112 | self.model.Update() 113 | self.model.SetParameterValue("ParamMouthOpenY", self.mouth_value, 1) 114 | 115 | self.update_gaze_tracking(width, height) 116 | 117 | self.model.Draw() 118 | glfw.swap_buffers(self.window) 119 | glfw.poll_events() 120 | 121 | pygame.mixer.music.stop() 122 | pygame.mixer.quit() 123 | dispose() 124 | glfw.terminate() 125 | 126 | def update_gaze_tracking(self, width, height): 127 | 128 | # 计算鼠标跟随逻辑,让 Live2D 角色的眼睛和头部跟随鼠标 129 | 130 | screen_x, screen_y = pyautogui.position() 131 | win_x, win_y = glfw.get_window_pos(self.window) 132 | local_mouse_x = screen_x - win_x 133 | local_mouse_y = screen_y - win_y 134 | 135 | if (screen_x != self.last_mouse_x) or (screen_y != self.last_mouse_y): 136 | self.last_move_time = time.time() 137 | self.last_mouse_x, self.last_mouse_y = screen_x, screen_y 138 | 139 | if (time.time() - self.last_move_time) < self.IDLE_THRESHOLD: 140 | mapped_x = self.X_MIN + (local_mouse_x / width) * (self.X_MAX - self.X_MIN) 141 | mapped_y = self.Y_MIN + (local_mouse_y / height) * (self.Y_MAX - self.Y_MIN) 142 | target_x = mapped_x 143 | target_y = mapped_y 144 | else: 145 | target_x = self.center_x_mapped 146 | target_y = self.center_y_mapped 147 | self.GAZE_EASING = 0.0004 148 | 149 | self.gaze_x += self.GAZE_EASING * (target_x - self.gaze_x) 150 | self.gaze_y += self.GAZE_EASING * (target_y - self.gaze_y) 151 | self.model.Drag(self.gaze_x, self.gaze_y) 152 | 153 | def extract_volume_array(self, audio_file): 154 | 155 | # 提取音频的音量信息,并归一化用于嘴型同步 156 | 157 | seg = AudioSegment.from_file(audio_file, format="wav") 158 | frame_duration_ms = 1000 / self.frame_rate 159 | num_frames = int(seg.duration_seconds * self.frame_rate) 160 | 161 | volumes = [] 162 | for i in range(num_frames): 163 | start_ms = i * frame_duration_ms 164 | frame_seg = seg[start_ms: start_ms + frame_duration_ms] 165 | rms = frame_seg.rms 166 | volumes.append(rms) 167 | 168 | max_rms = max(volumes) if volumes else 1 169 | volumes = [v / max_rms for v in volumes] # 归一化 170 | return volumes, seg.duration_seconds 171 | 172 | def play_audio_and_print_mouth(self, audio_file): 173 | 174 | # 播放音频并同步嘴型动作 175 | 176 | volume_array, audio_duration = self.extract_volume_array(audio_file) 177 | total_frames = len(volume_array) 178 | 179 | pygame.mixer.init() 180 | pygame.mixer.music.load(audio_file) 181 | pygame.mixer.music.play() 182 | 183 | start_time = time.time() 184 | while True: 185 | current_time = time.time() - start_time 186 | if current_time >= audio_duration: 187 | break 188 | 189 | frame_index = int(current_time * self.frame_rate) 190 | if frame_index >= total_frames: 191 | frame_index = total_frames - 1 192 | 193 | self.mouth_value = volume_array[frame_index] 194 | 195 | pygame.mixer.music.stop() 196 | 197 | 198 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | accelerate==1.4.0 2 | aiofiles==23.2.1 3 | aiohappyeyeballs==2.4.6 4 | aiohttp==3.11.12 5 | aiosignal==1.3.2 6 | anaconda-anon-usage @ file:///C:/b/abs_e8r_zga7xy/croot/anaconda-anon-usage_1732732454901/work 7 | annotated-types @ file:///C:/b/abs_0dmaoyhhj3/croot/annotated-types_1709542968311/work 8 | antlr4-python3-runtime==4.9.3 9 | anyio==4.8.0 10 | archspec @ file:///croot/archspec_1709217642129/work 11 | attrs==25.1.0 12 | audioread==3.0.1 13 | beautifulsoup4==4.13.3 14 | boltons @ file:///C:/b/abs_45_52ughkz/croot/boltons_1737061711836/work 15 | Brotli @ file:///C:/b/abs_c415aux9ra/croot/brotli-split_1736182803933/work 16 | certifi @ file:///C:/b/abs_8a944p1_gn/croot/certifi_1738623753421/work/certifi 17 | cffi @ file:///C:/b/abs_29_b57if3f/croot/cffi_1736184144340/work 18 | charset-normalizer @ file:///croot/charset-normalizer_1721748349566/work 19 | click==8.1.8 20 | colorama @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/colorama_1699472650914/work 21 | coloredlogs==15.0.1 22 | conda @ file:///D:/bld/conda_1739917047096/work 23 | conda-anaconda-telemetry @ file:///C:/b/abs_4c9llcc5ob/croot/conda-anaconda-telemetry_1736524617431/work 24 | conda-anaconda-tos @ file:///C:/b/abs_ceeuq0lee_/croot/conda-anaconda-tos_1739299022910/work 25 | conda-content-trust @ file:///C:/b/abs_bdfatn_wzf/croot/conda-content-trust_1714483201909/work 26 | conda-libmamba-solver @ file:///croot/conda-libmamba-solver_1737733694612/work/src 27 | conda-package-handling @ file:///C:/b/abs_7fz3aferfv/croot/conda-package-handling_1731369038903/work 28 | conda_package_streaming @ file:///C:/b/abs_bdz9vbvbh2/croot/conda-package-streaming_1731366449946/work 29 | conformer==0.3.2 30 | contourpy==1.3.1 31 | cryptography @ file:///C:/b/abs_e2lzchf4i6/croot/cryptography_1732130411942/work 32 | cycler==0.12.1 33 | Cython==3.0.12 34 | decorator==5.1.1 35 | diffusers==0.32.2 36 | distro @ file:///C:/b/abs_71xr36ua5r/croot/distro_1714488282676/work 37 | einops==0.8.1 38 | fastapi==0.115.8 39 | ffmpy==0.5.0 40 | filelock==3.17.0 41 | flatbuffers @ file:///home/conda/feedstock_root/build_artifacts/python-flatbuffers_1739279199749/work 42 | fonttools==4.56.0 43 | frozendict @ file:///C:/b/abs_2alamqss6p/croot/frozendict_1713194885124/work 44 | frozenlist==1.5.0 45 | fsspec==2025.2.0 46 | gdown==5.2.0 47 | gradio==5.16.1 48 | gradio_client==1.7.0 49 | grpcio @ file:///D:/bld/grpc-split_1713388447196/work 50 | grpcio-tools @ file:///D:/bld/grpcio-tools_1713479862547/work 51 | h11==0.14.0 52 | httpcore==1.0.7 53 | httpx==0.28.1 54 | huggingface-hub==0.28.1 55 | humanfriendly==10.0 56 | hydra-core==1.3.2 57 | HyperPyYAML==1.2.2 58 | idna @ file:///C:/b/abs_aad84bnnw5/croot/idna_1714398896795/work 59 | importlib_metadata==8.6.1 60 | importlib_resources==6.5.2 61 | inflect==7.5.0 62 | Jinja2==3.1.5 63 | joblib==1.4.2 64 | jsonpatch @ file:///C:/b/abs_4fdm88t7zi/croot/jsonpatch_1714483974578/work 65 | jsonpointer==2.1 66 | kiwisolver==1.4.8 67 | lazy_loader==0.4 68 | libmambapy @ file:///C:/b/abs_627vsv8bhu/croot/mamba-split_1734469608328/work/libmambapy 69 | librosa==0.10.2.post1 70 | lightning==2.5.0.post0 71 | lightning-utilities==0.12.0 72 | llvmlite==0.44.0 73 | markdown-it-py @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/markdown-it-py_1699473886965/work 74 | MarkupSafe==2.1.5 75 | matcha==0.3 76 | matplotlib==3.10.0 77 | mdurl @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/mdurl_1699473506455/work 78 | menuinst @ file:///C:/b/abs_fblttj5gp1/croot/menuinst_1738943438301/work 79 | mkl-service==2.4.0 80 | mkl_fft @ file:///C:/Users/dev-admin/mkl/mkl_fft_1730823082242/work 81 | mkl_random @ file:///C:/Users/dev-admin/mkl/mkl_random_1730822522280/work 82 | modelscope==1.23.0 83 | more-itertools==10.6.0 84 | mpmath==1.3.0 85 | msgpack==1.1.0 86 | multidict==6.1.0 87 | networkx==3.4.2 88 | numba==0.61.0 89 | numpy @ file:///C:/b/abs_c1ywpu18ar/croot/numpy_and_numpy_base_1708638681471/work/dist/numpy-1.26.4-cp312-cp312-win_amd64.whl#sha256=becc06674317799ad0165a939a7613809d0bee9bd328a1e4308c57c39cacf08c 90 | omegaconf==2.3.0 91 | onnx @ file:///C:/b/abs_26fcas53j4/croot/onnx_1722521784627/work 92 | onnxruntime-gpu @ file:///C:/Users/mark/miniforge3/conda-bld/onnxruntime_1735406817872/work/build-ci/Release/dist/onnxruntime_gpu-1.20.1-cp312-cp312-win_amd64.whl#sha256=00277ed6954e6c51eaa62089eee91a9ec2ba8097078adf00994717f8b0de0c1d 93 | openai-whisper==20240930 94 | orjson==3.10.15 95 | packaging @ file:///C:/b/abs_3by6s2fa66/croot/packaging_1734472138782/work 96 | pandas==2.2.3 97 | peft==0.14.0 98 | pillow==11.1.0 99 | platformdirs @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/platformdirs_1701797392447/work 100 | pluggy @ file:///C:/b/abs_dfec_m79vo/croot/pluggy_1733170145382/work 101 | pooch==1.8.2 102 | propcache==0.2.1 103 | protobuf==4.25.3 104 | psutil==7.0.0 105 | pyarrow==19.0.1 106 | pycosat @ file:///C:/b/abs_18nblzzn70/croot/pycosat_1736868434419/work 107 | pycparser @ file:///tmp/build/80754af9/pycparser_1636541352034/work 108 | pydantic @ file:///C:/b/abs_27dx58x550/croot/pydantic_1734736090499/work 109 | pydantic_core @ file:///C:/b/abs_bdosz7qwys/croot/pydantic-core_1734726071532/work 110 | pydub==0.25.1 111 | Pygments @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/pygments_1699474141968/work 112 | pynini @ file:///D:/bld/pynini_1696660993031/work 113 | pyparsing==3.2.1 114 | pyreadline3==3.5.4 115 | PySocks @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/pysocks_1699473336188/work 116 | python-dateutil==2.9.0.post0 117 | python-multipart==0.0.20 118 | pytorch-lightning==2.5.0.post0 119 | pytz==2025.1 120 | pyworld==0.3.5 121 | PyYAML==6.0.2 122 | regex==2024.11.6 123 | requests @ file:///C:/b/abs_c3508vg8ez/croot/requests_1731000584867/work 124 | rich @ file:///C:/b/abs_21nw9z7xby/croot/rich_1720637504376/work 125 | ruamel.yaml @ file:///C:/b/abs_0cunwx_ww6/croot/ruamel.yaml_1727980181547/work 126 | ruamel.yaml.clib @ file:///C:/b/abs_5fk8zi6n09/croot/ruamel.yaml.clib_1727769837359/work 127 | ruff==0.9.6 128 | safehttpx==0.1.6 129 | safetensors==0.5.2 130 | scikit-learn==1.6.1 131 | scipy==1.15.2 132 | semantic-version==2.10.0 133 | setuptools==75.8.0 134 | shellingham==1.5.4 135 | six==1.17.0 136 | sniffio==1.3.1 137 | soundfile==0.13.1 138 | soupsieve==2.6 139 | soxr==0.5.0.post1 140 | starlette==0.45.3 141 | sympy==1.13.1 142 | threadpoolctl==3.5.0 143 | tiktoken==0.9.0 144 | tn==0.0.4 145 | tokenizers==0.21.0 146 | tomlkit==0.13.2 147 | torch==2.6.0 148 | torchaudio==2.6.0 149 | torchmetrics==1.6.1 150 | tqdm @ file:///C:/b/abs_0eh9b6xugj/croot/tqdm_1738945553987/work 151 | transformers==4.49.0 152 | truststore @ file:///C:/b/abs_494cm143zh/croot/truststore_1736550137835/work 153 | ttsfrd-dependency @ file:///E:/PyCharm/project/project3/CosyVoice/pretrained_models/CosyVoice-ttsfrd/ttsfrd_dependency-0.1-py3-none-any.whl#sha256=060a53f0650d12839983afdcfb052b049d7cf5c62344a00fee3a7344582aaf6f 154 | typeguard==4.4.2 155 | typer==0.15.1 156 | typing_extensions @ file:///C:/b/abs_0ffjxtihug/croot/typing_extensions_1734714875646/work 157 | tzdata==2025.1 158 | urllib3 @ file:///C:/b/abs_7bst06lizn/croot/urllib3_1737133657081/work 159 | uvicorn==0.34.0 160 | websockets==14.2 161 | WeTextProcessing==1.0.4.1 162 | wget==3.2 163 | wheel==0.45.1 164 | win-inet-pton @ file:///C:/Users/dev-admin/perseverance-python-buildout/croot/win_inet_pton_1699472992992/work 165 | yarl==1.18.3 166 | zipp==3.21.0 167 | zstandard @ file:///C:/b/abs_31t8xmrv_h/croot/zstandard_1731356578015/work 168 | -------------------------------------------------------------------------------- /Live2d_env/pachirisu anime girl - top half.physics3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Meta": { 4 | "PhysicsSettingCount": 5, 5 | "TotalInputCount": 15, 6 | "TotalOutputCount": 6, 7 | "VertexCount": 13, 8 | "Fps": 30, 9 | "EffectiveForces": { 10 | "Gravity": { 11 | "X": 0, 12 | "Y": -1 13 | }, 14 | "Wind": { 15 | "X": 0, 16 | "Y": 0 17 | } 18 | }, 19 | "PhysicsDictionary": [ 20 | { 21 | "Id": "PhysicsSetting1", 22 | "Name": "Ears Twitch" 23 | }, 24 | { 25 | "Id": "PhysicsSetting2", 26 | "Name": "Front Hair" 27 | }, 28 | { 29 | "Id": "PhysicsSetting3", 30 | "Name": "Side Hair" 31 | }, 32 | { 33 | "Id": "PhysicsSetting4", 34 | "Name": "Back Hair" 35 | }, 36 | { 37 | "Id": "PhysicsSetting5", 38 | "Name": "Ribbon" 39 | } 40 | ] 41 | }, 42 | "PhysicsSettings": [ 43 | { 44 | "Id": "PhysicsSetting1", 45 | "Input": [ 46 | { 47 | "Source": { 48 | "Target": "Parameter", 49 | "Id": "ParamEyeROpen" 50 | }, 51 | "Weight": 100, 52 | "Type": "X", 53 | "Reflect": false 54 | } 55 | ], 56 | "Output": [ 57 | { 58 | "Destination": { 59 | "Target": "Parameter", 60 | "Id": "Param" 61 | }, 62 | "VertexIndex": 1, 63 | "Scale": 0.567, 64 | "Weight": 100, 65 | "Type": "Angle", 66 | "Reflect": false 67 | }, 68 | { 69 | "Destination": { 70 | "Target": "Parameter", 71 | "Id": "AhogeTwitch" 72 | }, 73 | "VertexIndex": 1, 74 | "Scale": 1, 75 | "Weight": 100, 76 | "Type": "Angle", 77 | "Reflect": false 78 | } 79 | ], 80 | "Vertices": [ 81 | { 82 | "Position": { 83 | "X": 0, 84 | "Y": 0 85 | }, 86 | "Mobility": 1, 87 | "Delay": 1, 88 | "Acceleration": 1, 89 | "Radius": 0 90 | }, 91 | { 92 | "Position": { 93 | "X": 0, 94 | "Y": 10.8 95 | }, 96 | "Mobility": 0.59, 97 | "Delay": 1, 98 | "Acceleration": 1, 99 | "Radius": 10.8 100 | } 101 | ], 102 | "Normalization": { 103 | "Position": { 104 | "Minimum": -10, 105 | "Default": 0, 106 | "Maximum": 10 107 | }, 108 | "Angle": { 109 | "Minimum": -10, 110 | "Default": 0, 111 | "Maximum": 10 112 | } 113 | } 114 | }, 115 | { 116 | "Id": "PhysicsSetting2", 117 | "Input": [ 118 | { 119 | "Source": { 120 | "Target": "Parameter", 121 | "Id": "ParamAngleX" 122 | }, 123 | "Weight": 60, 124 | "Type": "X", 125 | "Reflect": false 126 | }, 127 | { 128 | "Source": { 129 | "Target": "Parameter", 130 | "Id": "ParamAngleZ" 131 | }, 132 | "Weight": 60, 133 | "Type": "Angle", 134 | "Reflect": false 135 | }, 136 | { 137 | "Source": { 138 | "Target": "Parameter", 139 | "Id": "ParamBodyAngleX" 140 | }, 141 | "Weight": 40, 142 | "Type": "X", 143 | "Reflect": false 144 | }, 145 | { 146 | "Source": { 147 | "Target": "Parameter", 148 | "Id": "ParamBodyAngleZ" 149 | }, 150 | "Weight": 40, 151 | "Type": "Angle", 152 | "Reflect": false 153 | } 154 | ], 155 | "Output": [ 156 | { 157 | "Destination": { 158 | "Target": "Parameter", 159 | "Id": "ParamHairFront" 160 | }, 161 | "VertexIndex": 1, 162 | "Scale": 1, 163 | "Weight": 100, 164 | "Type": "Angle", 165 | "Reflect": false 166 | } 167 | ], 168 | "Vertices": [ 169 | { 170 | "Position": { 171 | "X": 0, 172 | "Y": 0 173 | }, 174 | "Mobility": 1, 175 | "Delay": 1, 176 | "Acceleration": 1, 177 | "Radius": 0 178 | }, 179 | { 180 | "Position": { 181 | "X": 0, 182 | "Y": 15 183 | }, 184 | "Mobility": 0.86, 185 | "Delay": 0.8, 186 | "Acceleration": 1.5, 187 | "Radius": 15 188 | } 189 | ], 190 | "Normalization": { 191 | "Position": { 192 | "Minimum": -10, 193 | "Default": 0, 194 | "Maximum": 10 195 | }, 196 | "Angle": { 197 | "Minimum": -10, 198 | "Default": 0, 199 | "Maximum": 10 200 | } 201 | } 202 | }, 203 | { 204 | "Id": "PhysicsSetting3", 205 | "Input": [ 206 | { 207 | "Source": { 208 | "Target": "Parameter", 209 | "Id": "ParamAngleX" 210 | }, 211 | "Weight": 60, 212 | "Type": "X", 213 | "Reflect": false 214 | }, 215 | { 216 | "Source": { 217 | "Target": "Parameter", 218 | "Id": "ParamAngleZ" 219 | }, 220 | "Weight": 60, 221 | "Type": "Angle", 222 | "Reflect": false 223 | }, 224 | { 225 | "Source": { 226 | "Target": "Parameter", 227 | "Id": "ParamBodyAngleX" 228 | }, 229 | "Weight": 40, 230 | "Type": "X", 231 | "Reflect": false 232 | }, 233 | { 234 | "Source": { 235 | "Target": "Parameter", 236 | "Id": "ParamBodyAngleZ" 237 | }, 238 | "Weight": 40, 239 | "Type": "Angle", 240 | "Reflect": false 241 | } 242 | ], 243 | "Output": [ 244 | { 245 | "Destination": { 246 | "Target": "Parameter", 247 | "Id": "ParamHairSide" 248 | }, 249 | "VertexIndex": 1, 250 | "Scale": 1, 251 | "Weight": 100, 252 | "Type": "Angle", 253 | "Reflect": false 254 | } 255 | ], 256 | "Vertices": [ 257 | { 258 | "Position": { 259 | "X": 0, 260 | "Y": 0 261 | }, 262 | "Mobility": 1, 263 | "Delay": 1, 264 | "Acceleration": 1, 265 | "Radius": 0 266 | }, 267 | { 268 | "Position": { 269 | "X": 0, 270 | "Y": 10 271 | }, 272 | "Mobility": 0.84, 273 | "Delay": 0.8, 274 | "Acceleration": 1.5, 275 | "Radius": 10 276 | }, 277 | { 278 | "Position": { 279 | "X": 0, 280 | "Y": 18 281 | }, 282 | "Mobility": 0.76, 283 | "Delay": 0.8, 284 | "Acceleration": 1.5, 285 | "Radius": 8 286 | } 287 | ], 288 | "Normalization": { 289 | "Position": { 290 | "Minimum": -10, 291 | "Default": 0, 292 | "Maximum": 10 293 | }, 294 | "Angle": { 295 | "Minimum": -10, 296 | "Default": 0, 297 | "Maximum": 10 298 | } 299 | } 300 | }, 301 | { 302 | "Id": "PhysicsSetting4", 303 | "Input": [ 304 | { 305 | "Source": { 306 | "Target": "Parameter", 307 | "Id": "ParamAngleX" 308 | }, 309 | "Weight": 60, 310 | "Type": "X", 311 | "Reflect": false 312 | }, 313 | { 314 | "Source": { 315 | "Target": "Parameter", 316 | "Id": "ParamAngleZ" 317 | }, 318 | "Weight": 60, 319 | "Type": "Angle", 320 | "Reflect": false 321 | }, 322 | { 323 | "Source": { 324 | "Target": "Parameter", 325 | "Id": "ParamBodyAngleX" 326 | }, 327 | "Weight": 40, 328 | "Type": "X", 329 | "Reflect": false 330 | }, 331 | { 332 | "Source": { 333 | "Target": "Parameter", 334 | "Id": "ParamBodyAngleZ" 335 | }, 336 | "Weight": 40, 337 | "Type": "Angle", 338 | "Reflect": false 339 | } 340 | ], 341 | "Output": [ 342 | { 343 | "Destination": { 344 | "Target": "Parameter", 345 | "Id": "ParamHairBack" 346 | }, 347 | "VertexIndex": 1, 348 | "Scale": 1, 349 | "Weight": 100, 350 | "Type": "Angle", 351 | "Reflect": false 352 | } 353 | ], 354 | "Vertices": [ 355 | { 356 | "Position": { 357 | "X": 0, 358 | "Y": 0 359 | }, 360 | "Mobility": 1, 361 | "Delay": 1, 362 | "Acceleration": 1, 363 | "Radius": 0 364 | }, 365 | { 366 | "Position": { 367 | "X": 0, 368 | "Y": 10 369 | }, 370 | "Mobility": 0.85, 371 | "Delay": 0.9, 372 | "Acceleration": 1, 373 | "Radius": 10 374 | }, 375 | { 376 | "Position": { 377 | "X": 0, 378 | "Y": 20 379 | }, 380 | "Mobility": 0.9, 381 | "Delay": 0.9, 382 | "Acceleration": 1, 383 | "Radius": 10 384 | }, 385 | { 386 | "Position": { 387 | "X": 0, 388 | "Y": 28 389 | }, 390 | "Mobility": 0.9, 391 | "Delay": 0.9, 392 | "Acceleration": 0.8, 393 | "Radius": 8 394 | } 395 | ], 396 | "Normalization": { 397 | "Position": { 398 | "Minimum": -10, 399 | "Default": 0, 400 | "Maximum": 10 401 | }, 402 | "Angle": { 403 | "Minimum": -10, 404 | "Default": 0, 405 | "Maximum": 10 406 | } 407 | } 408 | }, 409 | { 410 | "Id": "PhysicsSetting5", 411 | "Input": [ 412 | { 413 | "Source": { 414 | "Target": "Parameter", 415 | "Id": "ParamBodyAngleX" 416 | }, 417 | "Weight": 100, 418 | "Type": "X", 419 | "Reflect": false 420 | }, 421 | { 422 | "Source": { 423 | "Target": "Parameter", 424 | "Id": "ParamBodyAngleZ" 425 | }, 426 | "Weight": 100, 427 | "Type": "Angle", 428 | "Reflect": false 429 | } 430 | ], 431 | "Output": [ 432 | { 433 | "Destination": { 434 | "Target": "Parameter", 435 | "Id": "RibbonPhysics" 436 | }, 437 | "VertexIndex": 1, 438 | "Scale": 1, 439 | "Weight": 100, 440 | "Type": "Angle", 441 | "Reflect": false 442 | } 443 | ], 444 | "Vertices": [ 445 | { 446 | "Position": { 447 | "X": 0, 448 | "Y": 0 449 | }, 450 | "Mobility": 1, 451 | "Delay": 1, 452 | "Acceleration": 1, 453 | "Radius": 0 454 | }, 455 | { 456 | "Position": { 457 | "X": 0, 458 | "Y": 10 459 | }, 460 | "Mobility": 0.9, 461 | "Delay": 0.6, 462 | "Acceleration": 1.5, 463 | "Radius": 10 464 | } 465 | ], 466 | "Normalization": { 467 | "Position": { 468 | "Minimum": -10, 469 | "Default": 0, 470 | "Maximum": 10 471 | }, 472 | "Angle": { 473 | "Minimum": -10, 474 | "Default": 0, 475 | "Maximum": 10 476 | } 477 | } 478 | } 479 | ] 480 | } -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Live2D-LLM-Chat 2 | [US English](README.md) | [CN 中文](README_CN.md) 3 | 4 | [](https://github.com/FunAudioLLM/SenseVoice) 5 | [](https://openai.com/api/) 6 | [](https://github.com/FunAudioLLM/CosyVoice) 7 | [](https://github.com/Arkueid/live2d-py) 8 | 9 | [](https://www.python.org/downloads/) 10 | [](https://docs.anaconda.net.cn/miniconda/install/) 11 | 12 | > **Live2D + ASR + LLM + TTS** → 实时语音互动 | 本地部署 / 云端推理 13 | 14 | --- 15 | ## ✨ 1. 项目简介 16 | 17 | **Live2D-LLM-Chat** 是一个集成了**Live2D 虚拟形象**、**语音识别(ASR)**、**大语言模型(LLM)**和**文本转语音(TTS)** 的实时 AI 交互项目。它能够让**虚拟角色**通过语音识别用户的输入,并使用 AI 生成智能回复,同时通过 TTS 播放语音,并驱动 Live2D 动画实现嘴型同步,达到自然的互动体验。 18 | 19 | --- 20 | ### 📌 1.1. 主要功能 21 | - 🎙 **语音识别(ASR)**:使用 FunASR 进行语音转文本 (STT) 处理。 22 | - 🧠 **大语言模型(LLM)**:基于 OpenAI GPT / DeepSeek 提供理性沟通能力。 23 | - 🔊 **文本转语音(TTS)**:使用 CosyVoice 实现高质量的合成语音 24 | - 🏆 **Live2D 虚拟形象交互**:使用 Live2D SDK 渲染角色,并实现模型的实时反馈。 25 | 26 | --- 27 | ### 📌 1.2. 优化功能 28 | - **LLM模块**接口可支持本地与云端部署,本地部署基于**LM Studio**接口,基本涵盖所有已开源模型,但个人设备性能难以运行大体量模型;云端部署接口现已支持**OpenAI**平台接口与**DeepSeek**平台接口。 29 | - 储存模型对话时的前文数据,形成**历史记忆**。每5次对话会进行总结,避免多次对话后文本累计过量的情况。 30 | - 对历次模型对话的时间与内容进行**存档**,便于查找过往对话内容。可存档内容包括模型的**历史语音输出**。该功能可在配置文件中关闭,关闭后再次进行对话时将清除历史对话的语音数据,**减清内存压力**。 31 | - 重构Live2d模型角色的**眼神跟随**与**眨眼逻辑**,即使live2d模型没有内置眨眼逻辑,也可自然眨眼。编写**嘴型变化**逻辑,读取TTS模块输出的音频文件,将实时音频大小转化至live2d模型的嘴型变化。 32 | - 修改CosyVoice模型的API接口程序,改变生成语音文件打开方式,允许**直接保存**生成文件;对于长文本下分段生成的语音文件,**合并**为单一文件。 33 |
34 |
35 |
36 | Live2D 运行展示
37 |
35 |
36 |
37 | Live2D Running Showcase
38 |