├── icons ├── ai.png ├── home.png ├── logo.ico ├── logo.png ├── audio.png ├── clone.png └── human.png ├── ui.bat ├── LICENSE.md ├── yunxinghudong.bat ├── shipinxunlian.bat ├── yunxing.bat ├── README.md ├── UI.spec ├── lianjie.py ├── jianting.py ├── style.qss ├── hudong.py └── ui.py /icons/ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/ai.png -------------------------------------------------------------------------------- /icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/home.png -------------------------------------------------------------------------------- /icons/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/logo.ico -------------------------------------------------------------------------------- /icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/logo.png -------------------------------------------------------------------------------- /icons/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/audio.png -------------------------------------------------------------------------------- /icons/clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/clone.png -------------------------------------------------------------------------------- /icons/human.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activivity/Lan-dao/HEAD/icons/human.png -------------------------------------------------------------------------------- /ui.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 启动 jianting.py... 3 | start python ui.py 4 | echo 数字人脚本已启动。 5 | pause -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | 基于WebSocket协议实现实时弹幕信息爬取与信息通信。通过MaxKB容器训练直播互动模型,具备智能互动能力,通过微调预训练的语言模型来适应特定的直播场景需求,提升数字人的交互体验。基于TTS和Wav2lip开发语音克隆和唇形同步算法,通过预训练数字人模型的方式压缩生成时间,并根据多模态数据(如表情、语言等)进行微调,优化模型的计算性能,保证数字人在高并发环境下的实时响应,使数字人交互速度达到实时交互水平(5s)。 3 | 4 | -------------------------------------------------------------------------------- /yunxinghudong.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 启动 hudong.py... 3 | start python hudong.py 4 | 5 | echo 启动 getDouyin 文件夹中的 main.py... 6 | start cmd /c "cd getDouyin && .\venv\Scripts\activate && python main.py" 7 | echo 互动脚本已启动。 8 | pause -------------------------------------------------------------------------------- /shipinxunlian.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM 切换到当前目录中的 DH_live-main 文件夹 3 | cd /d "%~dp0DH_live-main" 4 | 5 | REM 激活 Conda 环境 6 | call conda activate dh_live 7 | 8 | REM 运行 xunlian.py 脚本 9 | python xunlian.py 10 | 11 | REM 暂停以防止窗口自动关闭 12 | pause 13 | -------------------------------------------------------------------------------- /yunxing.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 启动 jianting.py... 3 | start python jianting.py 4 | 5 | echo 启动 GPT-SoVITS-v2 文件夹中的 推理_并行.bat... 6 | start cmd /c "cd GPT-SoVITS-v2 && call tuili.bat" 7 | 8 | echo 启动 DH_live 文件夹中的 jiankong.py... 9 | start cmd /k "cd DH_live-main && call jiankong.bat" 10 | 11 | echo 数字人脚本已启动。 12 | pause 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 通过MaxKB容器训练直播互动模型,具备智能互动能力,通过微调预训练的语言模型来适应特定的直播场景需求,提升数字人的交互体验。基于TTS和Wav2lip开发语音克隆和唇形同步算法,通过预训练数字人模型的方式压缩生成时间,并根据多模态数据(如表情、语言等)进行微调,优化模型的计算性能,保证数字人在高并发环境下的实时响应,使数字人交互速度达到实时交互水平(5s)。 2 | 3 | 使用逻辑 4 | 5 | 三种启动模式 6 | 7 | 1 只启动数字人系统。 8 | 9 | 首先在数字人模型切换下拉框中选择要使用的数字人模型,并点击确认更换数字人模型。然后点击主界面里的启动数字人系统按钮,初始化tts算法和唇形替换算法。然后可以把想要生成的文案输入到输入框内,点击确定生成数字人,程序会开始运行。生成出来的视频会保存在DH_live-main/shuchu/shuchu.mp4 10 | 11 | 2 只启动互动系统 12 | 13 | 首先在主界面顶部输入要进行ai互动的直播间链接(一般是自己的直播间链接),然后点击主界面的中的启动互动系统按钮,启动互动系统。程序会实时爬取直播间内观众的弹幕信息,并调用本地大模型进行智能回复(本地大模型可以根据直播间内容,商品信息等做微调训练) 14 | 15 | 3 同时启动互动系统和数字人生成系统 16 | 17 | 按照上面的步骤同时启用互动系统和数字人生成系统。程序会自动爬取直播间弹幕,并调用本地大模型进行智能回复,回复的内容会作为文本输入,生成实时回复的数字人视频。 18 | -------------------------------------------------------------------------------- /UI.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( ['ui.py',"hudong.py", "jianting.py",], 5 | pathex=[], 6 | binaries=[], 7 | datas=[], 8 | hiddenimports=[], 9 | hookspath=[], 10 | hooksconfig={}, 11 | runtime_hooks=[], 12 | excludes=[], 13 | noarchive=False, 14 | optimize=0, 15 | ) 16 | pyz = PYZ(a.pure) 17 | 18 | exe = EXE( 19 | pyz, 20 | a.scripts, 21 | [], 22 | exclude_binaries=True, 23 | name='UI', 24 | debug=False, 25 | bootloader_ignore_signals=False, 26 | strip=False, 27 | upx=True, 28 | console=False, 29 | disable_windowed_traceback=False, 30 | argv_emulation=False, 31 | target_arch=None, 32 | codesign_identity=None, 33 | entitlements_file=None, 34 | icon="icons/logo.ico" 35 | ) 36 | coll = COLLECT( 37 | exe, 38 | a.binaries, 39 | a.datas, 40 | strip=False, 41 | upx=True, 42 | upx_exclude=[], 43 | name='UI', 44 | ) 45 | -------------------------------------------------------------------------------- /lianjie.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from selenium import webdriver 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.chrome.service import Service 6 | from webdriver_manager.chrome import ChromeDriverManager 7 | from selenium.webdriver.chrome.options import Options 8 | import time 9 | import re 10 | 11 | # 创建 ChromeOptions 对象,启用日志记录功能 12 | chrome_options = Options() 13 | chrome_options.add_argument("--auto-open-devtools-for-tabs") # 开启开发者工具(可选) 14 | chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"}) # 启用性能日志 15 | 16 | # 使用 webdriver_manager 管理 ChromeDriver 17 | driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) 18 | 19 | # 打开直播间页面 20 | driver.get("https://live.douyin.com/211374705237") # 替换为直播间链接 21 | 22 | # 等待页面加载 23 | time.sleep(5) # 根据实际需要调整 24 | 25 | # 切换到 iframe,如果页面使用了嵌套的 iframe 26 | iframe = driver.find_element(By.TAG_NAME, 'iframe') # 使用新的 API 27 | driver.switch_to.frame(iframe) 28 | 29 | # 等待页面加载 30 | time.sleep(5) 31 | 32 | # 获取性能日志 33 | logs = driver.get_log("performance") 34 | 35 | # 查找 WebSocket 连接地址 36 | websocket_url = None 37 | for log in logs: 38 | message = log["message"] 39 | if "Network.webSocketCreated" in message: 40 | # 尝试从日志中提取 WebSocket 地址 41 | ws_match = re.search(r'"url":"(ws://[^\s"]+|wss://[^\s"]+)"', message) 42 | if ws_match: 43 | websocket_url = ws_match.group(1) 44 | break 45 | 46 | if websocket_url: 47 | print(f"WebSocket 连接地址: {websocket_url}") 48 | else: 49 | print("未找到 WebSocket 地址") 50 | 51 | # 切换回主页面 52 | driver.switch_to.default_content() 53 | 54 | # 关闭浏览器 55 | driver.quit() 56 | -------------------------------------------------------------------------------- /jianting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import shutil 4 | import subprocess 5 | from watchdog.observers import Observer 6 | from watchdog.events import FileSystemEventHandler 7 | 8 | # 获取脚本所在目录的路径 9 | script_dir = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | # 定义监听的文件夹路径和目标文件夹路径 12 | source_folder = os.path.abspath(os.path.join(script_dir, 'GPT-SoVITS-v2', '音频输出')) 13 | destination_folder = os.path.abspath(os.path.join(script_dir, 'DH_live-main', "audio")) 14 | 15 | # 确保目标文件夹存在 16 | if not os.path.exists(destination_folder): 17 | os.makedirs(destination_folder) 18 | 19 | # 定义事件处理器类 20 | class AudioFileHandler(FileSystemEventHandler): 21 | def on_modified(self, event): 22 | if not event.is_directory and event.src_path.endswith(('.wav', '.mp3')): 23 | print(f"检测到文件修改: {event.src_path}") 24 | time.sleep(2) # 确保文件写入完成 25 | self.convert_audio_bitrate(event.src_path) 26 | 27 | def convert_audio_bitrate(self, src_path): 28 | file_name = os.path.basename(src_path) 29 | destination_path = os.path.join(destination_folder, file_name) 30 | temp_file = os.path.join(destination_folder, "temp_" + file_name) 31 | 32 | print(f"正在将文件 {src_path} 转换为 256 kbps, 16kHz 采样率, 单声道 16 位...") 33 | command = [ 34 | ffmpeg_path, '-i', src_path, 35 | '-b:a', '256k', '-ar', '16000', '-ac', '1', '-sample_fmt', 's16', temp_file 36 | ] 37 | try: 38 | subprocess.run(command, check=True) 39 | shutil.move(temp_file, destination_path) 40 | print(f"已将文件 {src_path} 转换并复制到: {destination_path}") 41 | except subprocess.CalledProcessError as e: 42 | print(f"ffmpeg转换失败: {e}") 43 | 44 | # 获取ffmpeg路径 45 | ffmpeg_path = os.path.join(script_dir, "ffmpeg","ffmpeg-master-latest-win64-gpl", "bin", 'ffmpeg.exe') 46 | 47 | # 创建观察者 48 | event_handler = AudioFileHandler() 49 | observer = Observer() 50 | observer.schedule(event_handler, path=source_folder, recursive=True) 51 | 52 | try: 53 | print(f"开始监听文件夹: {source_folder}") 54 | observer.start() 55 | while True: 56 | time.sleep(1) 57 | 58 | except KeyboardInterrupt: 59 | print("停止监听...") 60 | observer.stop() 61 | 62 | observer.join() 63 | -------------------------------------------------------------------------------- /style.qss: -------------------------------------------------------------------------------- 1 | /* 主窗口背景 */ 2 | QWidget { 3 | background-color: #f5f5f5; 4 | font-family: "Microsoft YaHei"; 5 | font-size: 14px; 6 | color: #333; 7 | border-radius: 8px; 8 | } 9 | 10 | /* 按钮样式 */ 11 | QPushButton { 12 | background-color: #5cb85c; 13 | color: white; 14 | font-size: 16px; 15 | border-radius: 8px; 16 | padding: 8px 16px; 17 | border: 1px solid #4cae4c; 18 | transition: background-color 0.3s ease; 19 | } 20 | 21 | QPushButton:hover { 22 | background-color: #4cae4c; 23 | } 24 | 25 | QPushButton:pressed { 26 | background-color: #3e8e41; 27 | } 28 | 29 | /* 标签样式 */ 30 | QLabel { 31 | color: #333; 32 | font-size: 14px; 33 | font-weight: bold; 34 | } 35 | 36 | /* 文本输入框 */ 37 | QLineEdit { 38 | border: 1px solid #ccc; 39 | border-radius: 5px; 40 | padding: 8px; 41 | background-color: #fff; 42 | } 43 | 44 | QLineEdit:focus { 45 | border-color: #3a82e4; 46 | background-color: #f0f8ff; 47 | } 48 | 49 | /* 下拉框 */ 50 | QComboBox { 51 | border: 1px solid #ccc; 52 | border-radius: 5px; 53 | padding: 8px; 54 | background-color: white; 55 | } 56 | 57 | QComboBox:hover { 58 | border-color: #3a82e4; 59 | } 60 | 61 | /* 列表控件样式 */ 62 | QListWidget { 63 | background-color: #fafafa; 64 | border: 1px solid #ccc; 65 | border-radius: 5px; 66 | font-size: 14px; 67 | } 68 | 69 | QListWidget::item { 70 | padding: 10px; 71 | } 72 | 73 | QListWidget::item:selected { 74 | background-color: #0078d7; 75 | color: white; 76 | } 77 | 78 | QListWidget::item:hover { 79 | background-color: #e0e0e0; 80 | } 81 | 82 | /* 滚动条样式 */ 83 | QScrollBar:vertical { 84 | width: 8px; 85 | background: #ddd; 86 | margin: 0px; 87 | padding-top: 9px; 88 | padding-bottom: 9px; 89 | } 90 | 91 | QScrollBar::handle:vertical { 92 | width: 8px; 93 | background: #999; 94 | border-radius: 4px; 95 | min-height: 20px; 96 | } 97 | 98 | QScrollBar::handle:vertical:hover { 99 | background: #777; 100 | } 101 | 102 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 103 | height: 9px; 104 | width: 8px; 105 | border: none; 106 | } 107 | 108 | /* 状态栏 */ 109 | QTextEdit { 110 | border: 1px solid #ccc; 111 | background-color: #fafafa; 112 | padding: 8px; 113 | border-radius: 5px; 114 | } 115 | 116 | /* 窗口美化 */ 117 | QMainWindow { 118 | background-color: #f7f7f7; 119 | border-radius: 10px; 120 | } 121 | -------------------------------------------------------------------------------- /hudong.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import time 4 | import os 5 | import subprocess 6 | 7 | # 基本配置 8 | base_url = "http://localhost:8080/api" 9 | headers = { 10 | "Authorization": "application-dba757fef57ede5eb74eb97b35dd33f5", # 替换为你的 API Key 11 | "Content-Type": "application/json" 12 | } 13 | 14 | # 启动 Docker Desktop 15 | def start_docker_desktop(): 16 | try: 17 | if os.name == 'nt': # 如果是 Windows 系统 18 | print("尝试启动 Docker Desktop...") 19 | subprocess.run(["start", "Docker Desktop"], shell=True) 20 | time.sleep(10) # 等待 Docker 启动 21 | print("Docker Desktop 启动中,等待初始化完成...") 22 | # 等待一段时间以确保 Docker 完全启动 23 | for _ in range(10): # 尝试 10 次,每次等待 5 秒 24 | result = subprocess.run(["docker", "info"], capture_output=True, text=True) 25 | if "Cannot connect to the Docker daemon" not in result.stderr: 26 | print("Docker 已启动。") 27 | return True 28 | time.sleep(5) 29 | print("无法连接 Docker Daemon,请检查 Docker Desktop 是否成功启动。") 30 | return False 31 | else: 32 | print("非 Windows 系统,请手动启动 Docker。") 33 | return False 34 | except Exception as e: 35 | print(f"启动 Docker Desktop 时出错: {e}") 36 | return False 37 | 38 | # 启动 Docker 容器或使用已有的容器 39 | def start_docker_and_maxkb(): 40 | try: 41 | # 检查 Docker Desktop 是否已经启动 42 | result = subprocess.run(["docker", "info"], capture_output=True, text=True) 43 | if "Cannot connect to the Docker daemon" in result.stderr: 44 | print("Docker Desktop 没有运行,正在尝试启动 Docker Desktop...") 45 | if not start_docker_desktop(): 46 | return False 47 | 48 | return manage_container() # 调用管理 Docker 容器的函数 49 | except subprocess.CalledProcessError as e: 50 | print(f"启动 Docker 或容器时出错: {e}") 51 | return False 52 | 53 | 54 | def manage_container(): 55 | try: 56 | # 获取当前脚本的绝对路径 57 | current_dir = os.path.dirname(os.path.abspath(__file__)) 58 | 59 | # 将相对路径 'maxkb' 转换为绝对路径 60 | data_path = os.path.join(current_dir, 'maxkb') 61 | abs_data_path = os.path.abspath(data_path) 62 | 63 | # 检查是否已经存在名为 maxkb 的容器 64 | result = subprocess.run(["docker", "ps", "-a", "-q", "-f", "name=maxkb"], capture_output=True, text=True) 65 | container_id = result.stdout.strip() 66 | 67 | if container_id: 68 | # 容器已存在,检查是否在运行 69 | running_result = subprocess.run(["docker", "ps", "-q", "-f", "name=maxkb"], capture_output=True, text=True) 70 | if running_result.stdout.strip(): 71 | print("maxkb 容器已在运行中。") 72 | else: 73 | print("正在启动已存在的 maxkb 容器...") 74 | subprocess.run(["docker", "start", "maxkb"], check=True) 75 | print("maxkb 容器启动成功。") 76 | else: 77 | # 容器不存在,创建一个新容器,挂载相对路径下的 maxkb 目录 78 | print("maxkb 容器不存在,正在创建并启动新容器...") 79 | subprocess.run([ 80 | "docker", "run", "-d", "--name=maxkb", "-p", "8080:8080", 81 | "-v", f"{abs_data_path}:/var/lib/postgresql/data", 82 | "1panel/maxkb" 83 | ], check=True) 84 | print("maxkb 容器创建并启动成功。") 85 | return True 86 | except subprocess.CalledProcessError as e: 87 | print(f"启动 Docker 或 maxkb 容器时出错: {e}") 88 | return False 89 | # 获取应用信息 90 | def get_application_info(): 91 | url = f"{base_url}/application/profile" 92 | response = requests.get(url, headers=headers) 93 | 94 | if response.status_code == 200: 95 | print("获取应用信息成功:", response.json()) 96 | return response.json() # 返回应用信息 97 | else: 98 | print(f"获取应用信息失败,状态码: {response.status_code}, 响应: {response.text}") 99 | return None 100 | 101 | # 打开会话 102 | def open_chat(application_id): 103 | url = f"{base_url}/application/{application_id}/chat/open" 104 | response = requests.get(url, headers=headers) 105 | 106 | if response.status_code == 200: 107 | print("会话开启成功:", response.json()) 108 | return response.json().get("data") # 获取 chat_id 109 | else: 110 | print(f"打开会话失败,状态码: {response.status_code}, 响应: {response.text}") 111 | return None 112 | 113 | # 发送消息并处理回复,将回复保存到txt文件 114 | def send_message(chat_id, message): 115 | url = f"{base_url}/application/chat_message/{chat_id}" 116 | 117 | # 在弹幕前加上指定的前导文字 118 | full_message = "下面是一场直播的弹幕,请对弹幕信息进行回答,使用中文回答,不需要说用户名,说公屏上看到有位宝子问,只对弹幕内容回复,请使用中文回答,不要回答表情: " + message 119 | 120 | data = { 121 | "message": full_message, 122 | "re_chat": False, 123 | "stream": True 124 | } 125 | 126 | response = requests.post(url, json=data, headers=headers) 127 | 128 | if response.status_code == 200: 129 | response_data = response.text.splitlines() 130 | full_reply = "" 131 | for line in response_data: 132 | try: 133 | chunk = line.strip().split(": ", 1)[-1] # 获取数据块 134 | if chunk: 135 | content_data = json.loads(chunk) 136 | full_reply += content_data.get("content", "") 137 | if content_data.get("is_end"): 138 | print("模型回复:", full_reply) 139 | save_reply_to_file(full_reply) 140 | break 141 | except json.JSONDecodeError as e: 142 | print(f"JSON 解析错误: {e}") 143 | print(f"无法解析的内容: {line}") 144 | else: 145 | print(f"发送消息失败,状态码: {response.status_code}, 响应: {response.text}") 146 | 147 | # 保存 AI 回复到 txt 文件 148 | def save_reply_to_file(reply): 149 | file_path = "GPT-SoVITS-v2/ai_reply.txt" 150 | with open(file_path, "w", encoding="utf-8") as file: 151 | file.write(reply) 152 | print(f"AI 的回复已保存到 {file_path}") 153 | 154 | # 监控 danmu.txt 文件 155 | def watch_danmu_file(chat_id, file_path): 156 | last_modify_time = os.path.getmtime(file_path) 157 | while True: 158 | current_modify_time = os.path.getmtime(file_path) 159 | if current_modify_time != last_modify_time: 160 | with open(file_path, 'r', encoding='utf-8') as file: 161 | danmu_message = file.read().strip() 162 | if danmu_message: 163 | print(f"文件更新,发送新弹幕: {danmu_message}") 164 | send_message(chat_id, danmu_message) 165 | last_modify_time = current_modify_time 166 | time.sleep(1) 167 | 168 | # 主流程 169 | def main(): 170 | if start_docker_and_maxkb(): # 确保 Docker 和 maxkb_new 容器已经启动 171 | application_info = get_application_info() 172 | if application_info: 173 | application_id = application_info.get("data", {}).get("id") 174 | if application_id: 175 | chat_id = open_chat(application_id) 176 | if chat_id: 177 | # 初始发送弹幕 178 | file_path = "getDouyin/danmu.txt" # 弹幕文件路径 179 | if os.path.exists(file_path): 180 | with open(file_path, 'r', encoding='utf-8') as file: 181 | danmu_message = file.read().strip() 182 | if danmu_message: 183 | send_message(chat_id, danmu_message) 184 | # 开始监控文件变化 185 | watch_danmu_file(chat_id, file_path) 186 | 187 | if __name__ == "__main__": 188 | main() 189 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import shutil 4 | import subprocess 5 | from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QFileDialog, QListWidget, QStackedWidget, QComboBox, QMessageBox, QTextEdit, QFrame, QGroupBox 6 | 7 | from PyQt5.QtWebEngineWidgets import QWebEngineView 8 | import qdarkgraystyle 9 | from PyQt5.QtCore import QUrl 10 | from PyQt5.QtCore import QTimer 11 | from PyQt5.QtWidgets import QListWidgetItem 12 | from PyQt5.QtGui import QIcon 13 | from PyQt5.QtCore import QProcess, Qt 14 | from qt_material import apply_stylesheet 15 | from PyQt5 import QtWidgets 16 | 17 | class MainWindow(QMainWindow): 18 | def __init__(self): 19 | super().__init__() 20 | 21 | 22 | # 创建主布局 23 | central_widget = QWidget() # 创建中央小部件 24 | self.setCentralWidget(central_widget) 25 | main_layout = QHBoxLayout(central_widget) # 定义主布局,并将其设置为中央小部件的布局 26 | self.setWindowTitle('数字人互动系统') 27 | self.setWindowIcon(QIcon('icons/logo.png')) 28 | # 创建侧边菜单 29 | self.menu_list = QListWidget() 30 | self.menu_list.setFixedWidth(200) # 增加侧边栏的宽度 31 | self.menu_list.setStyleSheet 32 | 33 | 34 | # 添加侧边栏菜单项并附加图标 35 | self.menu_list.addItem(QListWidgetItem(QIcon("icons/home.png"), "数字人生成")) 36 | self.menu_list.addItem(QListWidgetItem(QIcon("icons/human.png"), "数字人训练")) 37 | self.menu_list.addItem(QListWidgetItem(QIcon("icons/audio.png"), "音频模型训练")) 38 | self.menu_list.addItem(QListWidgetItem(QIcon("icons/clone.png"), "音频克隆")) 39 | self.menu_list.addItem(QListWidgetItem(QIcon("icons/ai.png"), "AI互动大模型训练")) 40 | # 响应侧边栏选项切换 41 | self.menu_list.currentRowChanged.connect(self.display_page) 42 | # 创建窗口容器(QStackedWidget) 43 | self.stack = QStackedWidget() 44 | 45 | # 添加页面 46 | self.stack.addWidget(self.create_home_page()) # 主窗口 47 | self.stack.addWidget(DigitalHumanTraining()) # 数字人训练页面 48 | self.stack.addWidget(self.create_audio_model_training_page()) # 音频模型训练页面 49 | self.stack.addWidget(self.create_audio_cloning_page()) # 音频克隆页面 50 | self.stack.addWidget(self.start_ai_model_training_page()) # ai训练 51 | 52 | # 将侧边菜单和窗口容器添加到主布局 53 | main_layout.addWidget(self.menu_list, 1) 54 | main_layout.addWidget(self.stack, 4) 55 | 56 | # 创建一个中央窗口来容纳主布局 57 | container = QWidget() 58 | container.setLayout(main_layout) 59 | self.setCentralWidget(container) 60 | # 右侧状态栏 61 | self.status_display = QTextEdit() 62 | self.status_display.setReadOnly(True) 63 | 64 | # 添加左侧菜单、窗口区域、右侧状态栏 65 | main_layout.addWidget(self.menu_list, 1) 66 | main_layout.addWidget(self.stack, 4) 67 | main_layout.addWidget(self.status_display, 2) 68 | 69 | # QProcess 处理 70 | self.process = QProcess(self) 71 | self.process.readyReadStandardOutput.connect(self.handle_stdout) 72 | self.process.readyReadStandardError.connect(self.handle_stderr) 73 | self.process.finished.connect(self.process_finished) 74 | 75 | def handle_stdout(self): 76 | """捕获标准输出""" 77 | data = self.process.readAllStandardOutput() 78 | text = data.data().decode() 79 | self.status_display.append(text) 80 | 81 | def handle_stderr(self): 82 | """捕获标准错误输出""" 83 | data = self.process.readAllStandardError() 84 | text = data.data().decode() 85 | self.status_display.append(f"{text}") 86 | 87 | def process_finished(self): 88 | """进程结束时显示""" 89 | self.status_display.append("进程结束。") 90 | 91 | def start_process(self, command): 92 | """启动进程,并捕获输出""" 93 | self.status_display.append(f"正在运行: {command}") 94 | self.process.start(command) 95 | 96 | def display_page(self, index): 97 | """切换显示不同的页面""" 98 | self.stack.setCurrentIndex(index) 99 | 100 | def create_home_page(self): 101 | """创建主窗口页面""" 102 | page = QWidget() 103 | layout = QVBoxLayout() 104 | layout.setSpacing(20) # 增加组件之间的间距 105 | 106 | # 使用 QGroupBox 创建模块分隔 107 | live_url_group = QGroupBox("") 108 | live_url_layout = QVBoxLayout() 109 | label_url = QLabel('请输入抖音直播间的 链接:') 110 | self.input_url = QLineEdit() 111 | self.input_url.setPlaceholderText("输入抖音直播间的链接") 112 | button_change_url = QPushButton('确定更改 直播间链接') 113 | button_change_url.clicked.connect(lambda: self.change_live_url(self.input_url.text())) 114 | 115 | live_url_layout.addWidget(label_url) 116 | live_url_layout.addWidget(self.input_url) 117 | live_url_layout.addWidget(button_change_url) 118 | live_url_group.setLayout(live_url_layout) 119 | 120 | # 创建分隔线 121 | separator = QFrame() 122 | separator.setFrameShape(QFrame.HLine) 123 | separator.setFrameShadow(QFrame.Sunken) 124 | 125 | # 将所有模块添加到布局 126 | layout.addWidget(live_url_group) 127 | layout.addWidget(separator) 128 | 129 | # 添加数字人模型选择模块 130 | model_label = QLabel('选择数字人模型:', self) 131 | self.model_dropdown = QComboBox(self) 132 | self.load_model_dropdown() # 加载模型列表 133 | confirm_button = QPushButton('确认选择模型', self) 134 | confirm_button.clicked.connect(self.on_model_selected) 135 | 136 | # 添加模型选择模块到布局 137 | layout.addWidget(model_label) 138 | layout.addWidget(self.model_dropdown) 139 | layout.addWidget(confirm_button) 140 | 141 | # 文本生成模块 142 | text_group = QGroupBox("文本生成") 143 | text_layout = QVBoxLayout() 144 | text_input_label = QLabel('直接输入文字生成数字人:') 145 | self.text_input = QTextEdit() 146 | button_generate_text = QPushButton('确认生成') 147 | button_generate_text.clicked.connect(lambda: self.generate_ai_reply_text(self.text_input.toPlainText())) 148 | 149 | text_layout.addWidget(text_input_label) 150 | text_layout.addWidget(self.text_input) 151 | text_layout.addWidget(button_generate_text) 152 | text_group.setLayout(text_layout) 153 | 154 | layout.addWidget(text_group) 155 | 156 | # 启动按钮 157 | button_layout = QHBoxLayout() 158 | self.button_start_digital_human = QPushButton('启动数字人') 159 | self.button_start_digital_human.clicked.connect(self.start_digital_human) # 绑定启动数字人的功能 160 | self.button_start_interactive_system = QPushButton('启动互动系统') 161 | self.button_start_interactive_system.clicked.connect(self.start_interactive_system) # 绑定启动互动系统的功能 162 | 163 | button_layout.addWidget(self.button_start_digital_human) 164 | button_layout.addWidget(self.button_start_interactive_system) 165 | 166 | layout.addLayout(button_layout) 167 | page.setLayout(layout) 168 | return page 169 | # create the application and the main window 170 | app = QtWidgets.QApplication(sys.argv) 171 | window = QtWidgets.QMainWindow() 172 | 173 | # setup stylesheet 174 | app.setStyleSheet(qdarkgraystyle.load_stylesheet()) 175 | 176 | 177 | 178 | def load_model_dropdown(self): 179 | """加载 video_data 文件夹中的所有子文件夹到下拉菜单""" 180 | video_data_path = os.path.join('DH_live-main', 'video_data') 181 | if os.path.exists(video_data_path): 182 | model_folders = [f for f in os.listdir(video_data_path) if os.path.isdir(os.path.join(video_data_path, f))] 183 | self.model_dropdown.addItems(model_folders) # 将子文件夹名称加入下拉菜单 184 | else: 185 | QMessageBox.warning(self, "错误", f"未找到路径: {video_data_path}") 186 | 187 | def on_model_selected(self): 188 | """当用户点击确认按钮时,更新 DH_live-main/jiankong.py 中的 moxing_file 变量""" 189 | selected_model = self.model_dropdown.currentText() 190 | if selected_model: 191 | jiankong_path = os.path.join('DH_live-main', 'jiankong.py') 192 | if os.path.exists(jiankong_path): 193 | try: 194 | with open(jiankong_path, 'r', encoding='utf-8') as file: 195 | lines = file.readlines() 196 | 197 | with open(jiankong_path, 'w', encoding='utf-8') as file: 198 | for line in lines: 199 | if line.startswith("moxing_file"): 200 | file.write(f'moxing_file = "{selected_model}"\n') 201 | else: 202 | file.write(line) 203 | 204 | QMessageBox.information(self, "成功", f"已确认选择模型并更新: {selected_model}") 205 | except Exception as e: 206 | QMessageBox.critical(self, "错误", f"更新 jiankong.py 失败: {str(e)}") 207 | else: 208 | QMessageBox.warning(self, "错误", "未找到 jiankong.py 文件") 209 | 210 | def change_live_url(self, new_url): 211 | """修改 config.py 中的 LIVE_URL 变量""" 212 | if new_url: 213 | try: 214 | config_path = os.path.join('getDouyin', 'config.py') # config.py 的路径 215 | with open(config_path, 'r', encoding='utf-8') as file: 216 | lines = file.readlines() 217 | 218 | with open(config_path, 'w', encoding='utf-8') as file: 219 | for line in lines: 220 | if line.startswith("LIVE_URL"): 221 | file.write(f'LIVE_URL = "{new_url}"\n') 222 | else: 223 | file.write(line) 224 | 225 | QMessageBox.information(self, "提示", "直播间链接 已成功更改") 226 | except Exception as e: 227 | QMessageBox.critical(self, "错误", f"修改失败: {str(e)}") 228 | else: 229 | QMessageBox.warning(self, "警告", "请输入有效的直播间链接") 230 | 231 | def generate_ai_reply_text(self, user_input_text): 232 | """生成文字到 ai_reply.txt 文件""" 233 | if user_input_text.strip(): 234 | try: 235 | ai_reply_file_path = os.path.join('GPT-SoVITS-v2', 'ai_reply.txt') 236 | with open(ai_reply_file_path, 'w', encoding='utf-8') as file: 237 | file.write(user_input_text) 238 | except Exception as e: 239 | QMessageBox.critical(self, "错误", f"更新 ai_reply.txt 失败: {str(e)}") 240 | else: 241 | QMessageBox.warning(self, "警告", "请输入内容以更新 ai_reply.txt") 242 | 243 | def start_digital_human(self): 244 | """启动数字人""" 245 | try: 246 | bat_path = os.path.abspath('yunxing.bat') # 获取 yunxing.bat 的绝对路径 247 | print(f"准备启动数字人,执行文件路径: {bat_path}") # 调试信息 248 | if os.path.exists(bat_path): 249 | print(f"文件 {bat_path} 存在,正在启动...") 250 | subprocess.Popen(['call', bat_path], shell=True) 251 | QMessageBox.information(self, "提示", "数字人已启动") 252 | else: 253 | print(f"文件 {bat_path} 不存在") 254 | QMessageBox.critical(self, "错误", f"启动失败:未找到 {bat_path}") 255 | except Exception as e: 256 | print(f"启动数字人失败: {e}") 257 | QMessageBox.critical(self, "错误", f"启动数字人失败: {str(e)}") 258 | 259 | def start_interactive_system(self): 260 | """启动互动系统,要求输入直播间链接""" 261 | live_url = self.input_url.text().strip() 262 | if live_url: 263 | try: 264 | bat_path = os.path.abspath('yunxinghudong.bat') # 获取 yunxinghudong.bat 的绝对路径 265 | print(f"准备启动互动系统,执行文件路径: {bat_path}") # 调试信息 266 | if os.path.exists(bat_path): 267 | print(f"文件 {bat_path} 存在,正在启动...") 268 | subprocess.Popen(['start', bat_path], shell=True) 269 | QMessageBox.information(self, "提示", "互动系统已启动") 270 | else: 271 | print(f"文件 {bat_path} 不存在") 272 | QMessageBox.critical(self, "错误", f"启动失败:未找到 {bat_path}") 273 | except Exception as e: 274 | print(f"启动互动系统失败: {e}") 275 | QMessageBox.critical(self, "错误", f"启动互动系统失败: {str(e)}") 276 | else: 277 | QMessageBox.warning(self, "警告", "请先输入直播间链接") 278 | 279 | 280 | def create_audio_model_training_page(self): 281 | """创建音频模型训练页面""" 282 | page = QWidget() 283 | layout = QVBoxLayout() 284 | 285 | # 添加启动音频模型训练按钮 286 | self.start_audio_training_button = QPushButton('启动音频模型训练', self) 287 | self.start_audio_training_button.clicked.connect(self.start_audio_model_training) 288 | 289 | # 添加浏览器窗口 290 | self.web_view = QWebEngineView(self) 291 | 292 | layout.addWidget(self.start_audio_training_button) 293 | layout.addWidget(self.web_view) 294 | page.setLayout(layout) 295 | return page 296 | 297 | def start_audio_model_training(self): 298 | """启动音频模型训练并在窗口中显示 Web UI""" 299 | try: 300 | # 构造 go-webui.bat 的绝对路径 301 | bat_path = os.path.abspath(os.path.join('GPT-SoVITS-v2', 'go-webui.bat')) 302 | 303 | # 检查 go-webui.bat 文件是否存在 304 | if os.path.exists(bat_path): 305 | # 启动 go-webui.bat,并确保工作目录是 GPT-SoVITS-v2 306 | subprocess.Popen(['start', bat_path], shell=True, cwd=os.path.dirname(bat_path)) 307 | 308 | # 打开本地运行的 Web UI 309 | self.web_view.setUrl(QUrl("http://localhost:9874/")) 310 | else: 311 | QMessageBox.critical(self, "错误", f"未找到批处理文件: {bat_path}") 312 | except Exception as e: 313 | QMessageBox.critical(self, "错误", f"启动音频模型训练失败: {str(e)}") 314 | 315 | def create_audio_cloning_page(self): 316 | """创建音频克隆页面(暂时为空白)""" 317 | page = QWidget() 318 | layout = QVBoxLayout() 319 | # 添加启动音频模型训练按钮 320 | self.start_audio_training_button = QPushButton('启动音频克隆', self) 321 | self.start_audio_training_button.clicked.connect(self.start_audio_model) 322 | 323 | # 添加浏览器窗口 324 | self.web_view = QWebEngineView(self) 325 | 326 | layout.addWidget(self.start_audio_training_button) 327 | layout.addWidget(self.web_view) 328 | page.setLayout(layout) 329 | return page 330 | 331 | def create_audio_cloning_page(self): 332 | """创建音频克隆页面""" 333 | page = QWidget() 334 | layout = QVBoxLayout() 335 | 336 | # 添加启动音频模型训练按钮 337 | self.start_audio_training_button = QPushButton('启动音频克隆', self) 338 | self.start_audio_training_button.clicked.connect(self.start_audio_model) 339 | 340 | # 添加浏览器窗口,用于显示网页内容 341 | self.audio_web_view = QWebEngineView(self) 342 | 343 | # 布局中添加按钮和浏览器视图 344 | layout.addWidget(self.start_audio_training_button) 345 | layout.addWidget(self.audio_web_view, stretch=1) 346 | page.setLayout(layout) 347 | return page 348 | 349 | def start_audio_model(self): 350 | """启动音频模型训练并在窗口中显示 Web UI""" 351 | try: 352 | bat_path = os.path.abspath(os.path.join('GPT-SoVITS-v2', 'tuili.bat')) 353 | 354 | if os.path.exists(bat_path): 355 | subprocess.Popen(bat_path, shell=True, cwd=os.path.dirname(bat_path)) 356 | self.audio_web_view.setUrl(QUrl("http://localhost:9872/")) # 加载音频克隆的网页 357 | else: 358 | QMessageBox.critical(self, "错误", f"未找到批处理文件: {bat_path}") 359 | except Exception as e: 360 | QMessageBox.critical(self, "错误", f"启动音频克隆失败: {str(e)}") 361 | 362 | def start_ai_model_training_page(self): 363 | """创建AI模型训练页面""" 364 | page = QWidget() 365 | layout = QVBoxLayout() 366 | 367 | # 添加启动 AI 模型训练按钮 368 | self.start_ai_training_button = QPushButton('启动 AI 模型训练', self) 369 | self.start_ai_training_button.clicked.connect(self.start_ai_model_training) 370 | 371 | # 添加浏览器窗口,用于显示网页内容 372 | self.ai_web_view = QWebEngineView(self) 373 | 374 | # 布局中添加按钮和浏览器视图 375 | layout.addWidget(self.start_ai_training_button) 376 | layout.addWidget(self.ai_web_view, stretch=1) 377 | page.setLayout(layout) 378 | return page 379 | 380 | def start_ai_model_training(self): 381 | # 获取当前脚本的绝对路径 382 | current_dir = os.path.dirname(os.path.abspath(__file__)) 383 | 384 | # 将相对路径 'maxkb' 转换为绝对路径 385 | data_path = os.path.join(current_dir, 'maxkb') 386 | abs_data_path = os.path.abspath(data_path) 387 | """启动 Docker 中的 maxkb 容器并在浏览器中显示页面""" 388 | try: 389 | result = subprocess.run(["docker", "ps", "-a", "-q", "-f", "name=maxkb"], capture_output=True, text=True) 390 | container_id = result.stdout.strip() 391 | 392 | if container_id: 393 | subprocess.run(["docker", "start", "maxkb"], check=True) 394 | else: 395 | subprocess.run([ 396 | "docker", "run", "-d", "--name=maxkb", "-p", "8080:8080", 397 | "-v", f"{abs_data_path}:/var/lib/postgresql/data", 398 | "1panel/maxkb" 399 | ], check=True) 400 | 401 | # 更新状态并加载网页 402 | self.ai_web_view.setHtml("

正在启动 maxkb 服务,请稍候...

") 403 | QTimer.singleShot(5000, self.load_maxkb_webpage) 404 | 405 | except subprocess.CalledProcessError as e: 406 | QMessageBox.critical(self, "错误", f"启动容器时出错: {str(e)}") 407 | 408 | def load_maxkb_webpage(self): 409 | """加载 maxkb 容器的 Web 界面""" 410 | self.ai_web_view.setUrl(QUrl("http://localhost:8080")) # 加载 AI 模型训练的网页 411 | 412 | 413 | class DigitalHumanTraining(QWidget): 414 | def __init__(self): 415 | super().__init__() 416 | self.video_path = None 417 | self.new_video_path = None 418 | self.dh_name = None 419 | 420 | # 创建标签、按钮和输入框 421 | self.upload_label = QLabel('', self) # 显示上传状态 422 | self.name_input = QLineEdit(self) 423 | self.name_input.setPlaceholderText("请输入数字人名称") 424 | self.button_train = QPushButton('训练', self) 425 | self.button_train.setEnabled(False) # 禁用训练按钮直到上传视频 426 | self.button_train.clicked.connect(self.run_training) 427 | 428 | # 视频上传按钮 429 | self.button_upload = QPushButton('上传视频', self) 430 | self.button_upload.clicked.connect(self.show_video_upload) 431 | 432 | # 布局设置 433 | layout = QVBoxLayout() 434 | layout.addWidget(QLabel('数字人训练页面', self)) 435 | layout.addWidget(self.button_upload) 436 | layout.addWidget(self.upload_label) 437 | layout.addWidget(self.name_input) 438 | layout.addWidget(self.button_train) 439 | 440 | self.setLayout(layout) 441 | 442 | def show_video_upload(self): 443 | """显示文件选择对话框,上传视频""" 444 | file_dialog = QFileDialog(self) 445 | file_dialog.setFileMode(QFileDialog.ExistingFile) 446 | file_dialog.setNameFilter("视频文件 (*.mp4 *.avi *.mov)") 447 | if file_dialog.exec_(): 448 | selected_file = file_dialog.selectedFiles()[0] 449 | self.video_path = selected_file 450 | self.upload_label.setText(f"已上传视频: {self.video_path}") 451 | self.button_train.setEnabled(True) # 启用训练按钮 452 | 453 | def run_training(self): 454 | """运行训练脚本""" 455 | self.dh_name = self.name_input.text().strip() 456 | if not self.dh_name: 457 | self.upload_label.setText("请先输入数字人名称") 458 | return 459 | 460 | yuan_folder = os.path.abspath(os.path.join('DH_live-main', 'yuan')) 461 | if not os.path.exists(yuan_folder): 462 | os.makedirs(yuan_folder) 463 | 464 | video_extension = os.path.splitext(self.video_path)[1] 465 | new_video_name = f"{self.dh_name}{video_extension}" 466 | self.new_video_path = os.path.join(yuan_folder, new_video_name) 467 | 468 | try: 469 | shutil.copy(self.video_path, self.new_video_path) 470 | self.upload_label.setText(f"视频已重命名并复制到: {self.new_video_path}") 471 | except Exception as e: 472 | self.upload_label.setText(f"视频复制失败: {str(e)}") 473 | return 474 | 475 | xunlian_path = os.path.abspath(os.path.join('DH_live-main', 'xunlian.py')) 476 | if not os.path.exists(xunlian_path): 477 | self.upload_label.setText(f"错误:找不到文件 {xunlian_path}") 478 | return 479 | 480 | try: 481 | with open(xunlian_path, 'r', encoding='utf-8') as file: 482 | lines = file.readlines() 483 | 484 | with open(xunlian_path, 'w', encoding='utf-8') as file: 485 | for line in lines: 486 | if line.startswith("video_file"): 487 | file.write(f'video_file = "yuan/{new_video_name}"\n') 488 | else: 489 | file.write(line) 490 | 491 | bat_path = os.path.abspath('shipinxunlian.bat') 492 | if os.path.exists(bat_path): 493 | subprocess.Popen(bat_path, shell=True) 494 | self.upload_label.setText(f'训练已启动,正在使用视频: {self.new_video_path}') 495 | else: 496 | self.upload_label.setText(f'错误:找不到文件 {bat_path}') 497 | except Exception as e: 498 | self.upload_label.setText(f'训练启动失败: {str(e)}') 499 | 500 | 501 | # 创建应用 502 | app = QApplication(sys.argv) 503 | 504 | # 创建主窗口 505 | window = MainWindow() 506 | window.show() 507 | 508 | # 运行应用 509 | sys.exit(app.exec_()) 510 | --------------------------------------------------------------------------------