| import os |
| import sys |
| import subprocess |
| import threading |
| import time |
| import socket |
| import logging |
| from fastapi import FastAPI |
| from fastapi.responses import FileResponse |
| from fastapi.staticfiles import StaticFiles |
| import uvicorn |
|
|
| |
| |
| |
| def resolve_ltx_path(): |
| import glob, tempfile, subprocess |
| sc_dir = os.path.join(os.getcwd(), "LTX_Shortcut") |
| os.makedirs(sc_dir, exist_ok=True) |
| lnk_files = glob.glob(os.path.join(sc_dir, "*.lnk")) |
| if not lnk_files: |
| print("\033[91m[ERROR] 未在 LTX_Shortcut 文件夹中找到快捷方式!\n请打开程序目录下的 LTX_Shortcut 文件夹,并将官方 LTX Desktop 的快捷方式复制进去后重试。\033[0m") |
| sys.exit(1) |
| |
| lnk_path = lnk_files[0] |
| |
| vbs_code = f'''Set sh = CreateObject("WScript.Shell")\nSet obj = sh.CreateShortcut("{os.path.abspath(lnk_path)}")\nWScript.Echo obj.TargetPath''' |
| fd, vbs_path = tempfile.mkstemp(suffix='.vbs') |
| with os.fdopen(fd, 'w') as f: |
| f.write(vbs_code) |
| try: |
| out = subprocess.check_output(['cscript', '//nologo', vbs_path], stderr=subprocess.STDOUT) |
| target_exe = out.decode('ansi').strip() |
| finally: |
| os.remove(vbs_path) |
| |
| if not target_exe or not os.path.exists(target_exe): |
| |
| default_paths = [ |
| os.path.join(os.environ.get("LOCALAPPDATA", ""), r"Programs\LTX Desktop\LTX Desktop.exe"), |
| r"C:\Program Files\LTX Desktop\LTX Desktop.exe", |
| r"D:\Program Files\LTX Desktop\LTX Desktop.exe", |
| r"E:\Program Files\LTX Desktop\LTX Desktop.exe" |
| ] |
| found = False |
| for p in default_paths: |
| if os.path.exists(p): |
| target_exe = p |
| print(f"\033[96m[INFO] 自动检测到 LTX 原版安装路径: {p}\033[0m") |
| found = True |
| break |
| |
| if not found: |
| print(f"\033[91m[ERROR] 未能找到原版 LTX Desktop 的安装路径!\033[0m") |
| print("请清理 LTX_Shortcut 文件夹,并将您当前电脑上真正的原版快捷方式重贴复制进去。") |
| sys.exit(1) |
| |
| return os.path.dirname(target_exe) |
|
|
| USER_PROFILE = os.path.expanduser("~") |
| PYTHON_EXE = os.path.join(USER_PROFILE, r"AppData\Local\LTXDesktop\python\python.exe") |
| DATA_DIR = os.path.join(USER_PROFILE, r"AppData\Local\LTXDesktop") |
|
|
| |
| LTX_INSTALL_DIR = resolve_ltx_path() |
| BACKEND_DIR = os.path.join(LTX_INSTALL_DIR, r"resources\backend") |
| UI_FILE_NAME = "UI/index.html" |
|
|
| |
| if not os.path.exists(PYTHON_EXE): |
| print(f"\n\033[1;41m [致命错误] 您的电脑上尚未配置好 LTX 的官方渲染核心框架! \033[0m") |
| print(f"\033[93m此应用仅是 UI 图形控制台,必需依赖原版软件环境才能生成。在 ({PYTHON_EXE}) 未找到运行引擎。\n") |
| print(">> 解决方案:\n1. 请先在您的电脑上正常安装【LTX Desktop 官方原版软件】。") |
| print("2. 必需:双击打开运行一次原版软件!(运行后原版软件会在后台自动释放环境)") |
| print("3. 把原版软件的快捷方式复制到本文档的 LTX_Shortcut 文件夹里面。") |
| print("4. 全部完成后,再重新启动本 run.bat 脚本即可!\033[0m\n") |
| os._exit(1) |
|
|
| |
| PATCHES_DIR = os.path.join(os.getcwd(), "patches") |
| os.makedirs(PATCHES_DIR, exist_ok=True) |
|
|
| |
| LOCAL_OUTPUTS = os.path.join(os.getcwd(), "outputs") |
| os.makedirs(LOCAL_OUTPUTS, exist_ok=True) |
|
|
| |
| os.makedirs(DATA_DIR, exist_ok=True) |
| with open(os.path.join(DATA_DIR, "custom_dir.txt"), 'w', encoding='utf-8') as f: |
| f.write(LOCAL_OUTPUTS) |
|
|
| os.environ["LTX_APP_DATA_DIR"] = DATA_DIR |
|
|
| |
| os.environ["PYTHONPATH"] = f"{PATCHES_DIR};{BACKEND_DIR}" |
|
|
| def get_lan_ip(): |
| try: |
| host_name = socket.gethostname() |
| _, _, ip_list = socket.gethostbyname_ex(host_name) |
| |
| candidates = [] |
| for ip in ip_list: |
| if ip.startswith("192.168."): |
| return ip |
| elif ip.startswith("10.") or (ip.startswith("172.") and 16 <= int(ip.split('.')[1]) <= 31): |
| candidates.append(ip) |
| |
| if candidates: |
| return candidates[0] |
| |
| |
| s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| s.connect(("8.8.8.8", 80)) |
| ip = s.getsockname()[0] |
| s.close() |
| return ip |
| except: |
| return "127.0.0.1" |
|
|
| LAN_IP = get_lan_ip() |
|
|
| |
| |
| |
| def check_port_in_use(port): |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| return s.connect_ex(('127.0.0.1', port)) == 0 |
|
|
| def launch_backend(): |
| """启动核心引擎 - 监听 0.0.0.0 确保局域网可调""" |
| if check_port_in_use(3000): |
| print(f"\n\033[1;41m [致命错误] 3000 端口已被占用,无法启动核心引擎! \033[0m") |
| print("\033[93m>> 绝大多数情况下,这是因为【官方原版 LTX Desktop】正在您的电脑后台运行。\033[0m") |
| print(">> 冲突会导致显存爆炸。请检查右下角系统托盘图标,右键完全退出官方软件。") |
| print(">> 退出后重新双击 run.bat 启动本程序!\n") |
| os._exit(1) |
|
|
| print(f"\033[96m[CORE] 核心引擎正在启动...\033[0m") |
| |
| import logging as _logging |
| _logging.basicConfig( |
| level=_logging.INFO, |
| format="[%(asctime)s] %(levelname)s %(name)s: %(message)s", |
| datefmt="%H:%M:%S", |
| force=True |
| ) |
| |
| |
| launcher_code = f""" |
| import sys |
| import os |
| |
| patch_dir = r"{PATCHES_DIR}" |
| backend_dir = r"{BACKEND_DIR}" |
| |
| # 防御性清除:强行剥离所有的默认 backend_dir 引用 |
| sys.path = [p for p in sys.path if p and os.path.normpath(p) != os.path.normpath(backend_dir)] |
| sys.path = [p for p in sys.path if p and p != "." and p != ""] |
| |
| # 绝对插队注入:优先搜索 PATCHES_DIR |
| sys.path.insert(0, patch_dir) |
| sys.path.insert(1, backend_dir) |
| |
| import uvicorn |
| from ltx2_server import app |
| |
| if __name__ == '__main__': |
| uvicorn.run(app, host="0.0.0.0", port=3000, log_level="info", access_log=False) |
| """ |
| launcher_path = os.path.join(PATCHES_DIR, "launcher.py") |
| with open(launcher_path, "w", encoding="utf-8") as f: |
| f.write(launcher_code) |
|
|
| cmd = [PYTHON_EXE, launcher_path] |
| env = os.environ.copy() |
| result = subprocess.run(cmd, cwd=BACKEND_DIR, env=env) |
| if result.returncode != 0: |
| print(f"\n\033[1;41m [致命错误] 核心引擎异常崩溃退出! (Exit Code: {result.returncode})\033[0m") |
| print(">> 请检查上述终端报错信息。确认显卡驱动是否正常。") |
| os._exit(1) |
|
|
| ui_app = FastAPI() |
| |
|
|
| @ui_app.get("/") |
| async def serve_index(): |
| return FileResponse(os.path.join(os.getcwd(), UI_FILE_NAME)) |
|
|
| @ui_app.get("/index.css") |
| async def serve_css(): |
| return FileResponse(os.path.join(os.getcwd(), "UI/index.css")) |
|
|
| @ui_app.get("/index.js") |
| async def serve_js(): |
| return FileResponse(os.path.join(os.getcwd(), "UI/index.js")) |
|
|
|
|
| @ui_app.get("/i18n.js") |
| async def serve_i18n(): |
| return FileResponse(os.path.join(os.getcwd(), "UI/i18n.js")) |
|
|
|
|
| def launch_ui_server(): |
| print(f"\033[92m[UI] 工作站已就绪!\033[0m") |
| print(f"\033[92m[LOCAL] 本机访问: http://127.0.0.1:4000\033[0m") |
| print(f"\033[93m[WIFI] 局域网访问: http://{LAN_IP}:4000\033[0m") |
| |
| |
| if sys.platform == 'win32': |
| |
| class UvicornAsyncioNoiseFilter(logging.Filter): |
| """压掉客户端断开、Win Proactor 管道收尾等无害 asyncio 控制台刷屏。""" |
|
|
| def filter(self, record): |
| if record.name != "asyncio": |
| return True |
| msg = record.getMessage() |
| if "_call_connection_lost" in msg or "_ProactorBasePipeTransport" in msg: |
| return False |
| if hasattr(record, "exc_info") and record.exc_info: |
| exc_type, exc_value, _ = record.exc_info |
| if isinstance(exc_value, ConnectionResetError) and getattr( |
| exc_value, "winerror", None |
| ) == 10054: |
| return False |
| if "10054" in msg and "ConnectionResetError" in msg: |
| return False |
| return True |
|
|
| logging.getLogger("asyncio").addFilter(UvicornAsyncioNoiseFilter()) |
| |
| uvicorn.run(ui_app, host="0.0.0.0", port=4000, log_level="warning", access_log=False) |
|
|
| if __name__ == "__main__": |
| os.system('cls' if os.name == 'nt' else 'clear') |
| print("\033[1;97;44m LTX-2 CINEMATIC WORKSTATION | NETWORK ENABLED \033[0m\n") |
| |
| threading.Thread(target=launch_backend, daemon=True).start() |
| |
| |
| print("\033[93m[SYS] 正在等待内部核心 3000 端口启动...\033[0m") |
| backend_ready = False |
| for _ in range(30): |
| try: |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| if s.connect_ex(('127.0.0.1', 3000)) == 0: |
| backend_ready = True |
| break |
| except Exception: |
| pass |
| time.sleep(1) |
| |
| if backend_ready: |
| print("\033[92m[SYS] 3000 端口已通过连通性握手验证!后端装载成功。\033[0m") |
| else: |
| print("\033[1;41m [崩坏警告] 等待 30 秒后,3000 端口依然无法连通! \033[0m") |
| print(">> Uvicorn 可能在后台陷入了死锁,或者被防火墙拦截,前端大概率将无法连接到后端!") |
| print(">> 请检查上方是否有 Python 报错。\n") |
| |
| try: |
| launch_ui_server() |
| except KeyboardInterrupt: |
| sys.exit(0) |