diff --git a/backend/config.py b/backend/config.py index 091ab9e..4de6b69 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,5 +1,6 @@ import os +import sys from pathlib import Path from qfluentwidgets import (qconfig, ConfigItem, QConfig, OptionsValidator, BoolValidator, OptionsConfigItem, EnumSerializer, RangeValidator, RangeConfigItem, ConfigValidator) @@ -115,10 +116,16 @@ elif isinstance(_detect_mode_value, str) and _detect_mode_value in ("精准", "P # 读取界面语言配置 tr = configparser.ConfigParser() -TRANSLATION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'interface', f"{config.interface.value}.ini") +# 确定运行环境(开发环境或打包环境) +if getattr(sys, 'frozen', False): + # 打包后的环境 + BASE_DIR = sys._MEIPASS + TRANSLATION_FILE = os.path.join(BASE_DIR, 'backend', 'interface', f"{config.interface.value}.ini") +else: + # 开发环境 + BASE_DIR = str(Path(os.path.abspath(__file__)).parent.parent) + TRANSLATION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'interface', f"{config.interface.value}.ini") + tr.read(TRANSLATION_FILE, encoding='utf-8') -# 项目的base目录 -BASE_DIR = str(Path(os.path.abspath(__file__)).parent) - os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' \ No newline at end of file diff --git a/backend/ffmpeg/win_x64/ffmpeg_1.exe b/backend/ffmpeg/win_x64/ffmpeg_1.exe deleted file mode 100644 index 5b75d5f..0000000 Binary files a/backend/ffmpeg/win_x64/ffmpeg_1.exe and /dev/null differ diff --git a/backend/ffmpeg/win_x64/ffmpeg_2.exe b/backend/ffmpeg/win_x64/ffmpeg_2.exe deleted file mode 100644 index 902df3f..0000000 Binary files a/backend/ffmpeg/win_x64/ffmpeg_2.exe and /dev/null differ diff --git a/backend/ffmpeg/win_x64/ffmpeg_3.exe b/backend/ffmpeg/win_x64/ffmpeg_3.exe deleted file mode 100644 index c0ff50e..0000000 Binary files a/backend/ffmpeg/win_x64/ffmpeg_3.exe and /dev/null differ diff --git a/backend/ffmpeg/win_x64/fs_manifest.csv b/backend/ffmpeg/win_x64/fs_manifest.csv deleted file mode 100644 index 501738d..0000000 --- a/backend/ffmpeg/win_x64/fs_manifest.csv +++ /dev/null @@ -1,4 +0,0 @@ -filename,filesize,encoding,header -ffmpeg_1.exe,50000000,, -ffmpeg_2.exe,50000000,, -ffmpeg_3.exe,13721856,, diff --git a/backend/models/big-lama/big-lama_1.pt b/backend/models/big-lama/big-lama_1.pt deleted file mode 100644 index 0085190..0000000 Binary files a/backend/models/big-lama/big-lama_1.pt and /dev/null differ diff --git a/backend/models/big-lama/big-lama_2.pt b/backend/models/big-lama/big-lama_2.pt deleted file mode 100644 index ca8e0ad..0000000 Binary files a/backend/models/big-lama/big-lama_2.pt and /dev/null differ diff --git a/backend/models/big-lama/big-lama_3.pt b/backend/models/big-lama/big-lama_3.pt deleted file mode 100644 index 063c1fb..0000000 Binary files a/backend/models/big-lama/big-lama_3.pt and /dev/null differ diff --git a/backend/models/big-lama/big-lama_4.pt b/backend/models/big-lama/big-lama_4.pt deleted file mode 100644 index 7e8fdcc..0000000 Binary files a/backend/models/big-lama/big-lama_4.pt and /dev/null differ diff --git a/backend/models/big-lama/big-lama_5.pt b/backend/models/big-lama/big-lama_5.pt deleted file mode 100644 index 9198d0b..0000000 Binary files a/backend/models/big-lama/big-lama_5.pt and /dev/null differ diff --git a/backend/models/big-lama/fs_manifest.csv b/backend/models/big-lama/fs_manifest.csv deleted file mode 100644 index 593582e..0000000 --- a/backend/models/big-lama/fs_manifest.csv +++ /dev/null @@ -1,6 +0,0 @@ -filename,filesize,encoding,header -big-lama_1.pt,50000000,, -big-lama_2.pt,50000000,, -big-lama_3.pt,50000000,, -big-lama_4.pt,50000000,, -big-lama_5.pt,5803670,, diff --git a/backend/tools/common_tools.py b/backend/tools/common_tools.py index 1cd1a71..64b7dd0 100644 --- a/backend/tools/common_tools.py +++ b/backend/tools/common_tools.py @@ -38,11 +38,15 @@ def is_video_or_image(filename): return file_extension in video_extensions or file_extension in image_extensions def merge_big_file_if_not_exists(dir, file, man_filename = None): - if file not in os.listdir(dir): - fs = Filesplit() - if man_filename is not None: - fs.man_filename = man_filename - fs.merge(input_dir=dir) + try: + if file not in os.listdir(dir): + fs = Filesplit() + if man_filename is not None: + fs.man_filename = man_filename + fs.merge(input_dir=dir) + except Exception as e: + print(f"Warning: Could not merge big file {file} in {dir}: {e}") + return False def get_readable_path(path): if sys.platform != 'win32': diff --git a/backend/tools/ffmpeg_cli.py b/backend/tools/ffmpeg_cli.py index 1c64fec..dd902ec 100644 --- a/backend/tools/ffmpeg_cli.py +++ b/backend/tools/ffmpeg_cli.py @@ -1,4 +1,5 @@ import os +import sys import stat import platform @@ -21,16 +22,29 @@ class FFmpegCLI: return cls._instance def __init__(self): - os.chmod(self.ffmpeg_path, stat.S_IRWXU + stat.S_IRWXG + stat.S_IRWXO) - + # 设置 FFmpeg 可执行文件权限 + try: + os.chmod(self.ffmpeg_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + except Exception as e: + print(f"Warning: Could not set ffmpeg executable permissions: {e}") + @property def ffmpeg_path(self): system = platform.system() + + # 确保路径正确(打包环境 vs 开发环境) + if getattr(sys, 'frozen', False): + # 打包环境:BASE_DIR 指向 sys._MEIPASS + base_path = os.path.join(BASE_DIR, 'backend') + else: + # 开发环境:BASE_DIR 已经是项目根目录 + base_path = BASE_DIR + if system == "Windows": - ffmpeg_dir = os.path.join(BASE_DIR, 'ffmpeg', 'win_x64') + ffmpeg_dir = os.path.join(base_path, 'ffmpeg', 'win_x64') merge_big_file_if_not_exists(ffmpeg_dir, 'ffmpeg.exe') return os.path.join(ffmpeg_dir, 'ffmpeg.exe') elif system == "Linux": - return os.path.join(BASE_DIR, 'ffmpeg', 'linux_x64', 'ffmpeg') + return os.path.join(base_path, 'ffmpeg', 'linux_x64', 'ffmpeg') else: - return os.path.join(BASE_DIR, 'ffmpeg', 'macos', 'ffmpeg') \ No newline at end of file + return os.path.join(base_path, 'ffmpeg', 'macos', 'ffmpeg') \ No newline at end of file diff --git a/backend/tools/model_config.py b/backend/tools/model_config.py index d5b9e71..08aa3a3 100644 --- a/backend/tools/model_config.py +++ b/backend/tools/model_config.py @@ -1,4 +1,5 @@ import os +import sys from backend.config import config, BASE_DIR from backend.tools.common_tools import merge_big_file_if_not_exists from backend.tools.constant import SubtitleDetectMode @@ -10,15 +11,34 @@ _MODEL_NAME_MAP = { class ModelConfig: def __init__(self): - self.LAMA_MODEL_DIR = os.path.join(BASE_DIR, 'models', 'big-lama') - self.STTN_AUTO_MODEL_PATH = os.path.join(BASE_DIR, 'models', 'sttn-auto', 'infer_model.pth') - self.STTN_DET_MODEL_PATH = os.path.join(BASE_DIR, 'models', 'sttn-det', 'sttn.pth') + # 确保模型路径正确(打包环境 vs 开发环境) + if getattr(sys, 'frozen', False): + # 打包环境:BASE_DIR 指向 sys._MEIPASS + model_base = os.path.join(BASE_DIR, 'backend') + else: + # 开发环境:BASE_DIR 已经是项目根目录 + model_base = BASE_DIR + + self.LAMA_MODEL_DIR = os.path.join(model_base, 'models', 'big-lama') + self.STTN_AUTO_MODEL_PATH = os.path.join(model_base, 'models', 'sttn-auto', 'infer_model.pth') + self.STTN_DET_MODEL_PATH = os.path.join(model_base, 'models', 'sttn-det', 'sttn.pth') if config.subtitleDetectMode.value == SubtitleDetectMode.PP_OCRv5_MOBILE: - self.DET_MODEL_DIR = os.path.join(BASE_DIR,'models', 'V5', 'ch_det_fast') + self.DET_MODEL_DIR = os.path.join(model_base,'models', 'V5', 'ch_det_fast') elif config.subtitleDetectMode.value == SubtitleDetectMode.PP_OCRv5_SERVER: - self.DET_MODEL_DIR = os.path.join(BASE_DIR, 'models', 'V5', 'ch_det') + self.DET_MODEL_DIR = os.path.join(model_base, 'models', 'V5', 'ch_det') else: raise ValueError(f"Invalid subtitle detect mode: {config.subtitleDetectMode.value}") self.DET_MODEL_NAME = _MODEL_NAME_MAP[config.subtitleDetectMode.value] - merge_big_file_if_not_exists(self.LAMA_MODEL_DIR, 'bit-lama.pt') + # 尝试合并大文件(如果需要) + lama_file = 'big-lama.pt' # 修正文件名:实际模型文件是 big-lama.pt + lama_file_path = os.path.join(self.LAMA_MODEL_DIR, lama_file) + if not os.path.exists(lama_file_path): + merge_big_file_if_not_exists(self.LAMA_MODEL_DIR, lama_file) + # 检查合并后文件是否存在 + if not os.path.exists(lama_file_path): + raise FileNotFoundError( + f"LAMA model file not found: {lama_file_path}. " + f"Please ensure the model file exists in {self.LAMA_MODEL_DIR}" + ) + diff --git a/build_pinstaller.py b/build_pinstaller.py new file mode 100644 index 0000000..fff422d --- /dev/null +++ b/build_pinstaller.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +基于官方 PaddleX 打包脚本改编的视频字幕去除器打包脚本 +""" + +import importlib.metadata +import argparse +import subprocess +import sys +import os + +def get_installed_packages(): + """获取当前环境中已安装的包""" + return [dist.metadata["Name"] for dist in importlib.metadata.distributions()] + +def build_package(main_file, include_cuda=False): + """构建打包命令""" + # 基础命令 + cmd = [ + "pyinstaller", main_file, + "--clean", + "--noconfirm", + "--name", "VideoSubtitleRemover", + "--icon", "design/vsr.ico", + "--console", # 先启用控制台以便调试 + ] + + # 收集数据文件 + cmd += [ + "--add-data", "backend/models/big-lama;backend/models/big-lama", + "--add-data", "backend/models/sttn-auto;backend/models/sttn-auto", + "--add-data", "backend/models/sttn-det;backend/models/sttn-det", + "--add-data", "backend/models/V5;backend/models/V5", + "--add-data", "backend/interface;backend/interface", + "--add-data", "backend/ffmpeg/win_x64/ffmpeg.exe;backend/ffmpeg/win_x64", + "--add-data", "config/config.json;config", + "--add-data", "design/vsr.ico;design", + ] + + # 收集重要包的数据和二进制文件 + cmd += [ + "--collect-data", "paddleocr", + "--collect-data", "skimage", + "--collect-binaries", "paddle", + "--collect-data", "opencv-python", + "--collect-data", "cv2", + ] + + # 隐藏导入 + hidden_imports = [ + # GUI 相关 + "PySide6.QtCore", + "PySide6.QtGui", + "PySide6.QtWidgets", + "PySide6.QtNetwork", + "qfluentwidgets", + "qframelesswindow", + + # 深度学习相关 + "torch", + "torchvision", + "paddle", + "paddleocr", + "paddleocr.ppocr", + "paddleocr.ppocr.api", + "paddleocr.ppocr.utils", + + # 图像处理 + "cv2", + "cv2.data", + "cv2.matplotlib", + "cv2.typing", + "numpy", + "PIL", + "scipy", + "scipy.spatial", + "scipy.spatial.transform", + "scipy.ndimage", + + # 视频处理 + "av", + + # 工具库 + "tqdm", + "requests", + "configparser", + "einops", + "darkdetect", + "je_showinfilemanager", + "filesplit", + + # 项目模块 + "backend", + "backend.inpaint", + "backend.inpaint.lama_inpaint", + "backend.inpaint.sttn_auto_inpaint", + "backend.inpaint.sttn_det_inpaint", + "backend.inpaint.opencv_inpaint", + "backend.tools", + "backend.tools.ocr", + "backend.tools.subtitle_detect", + "backend.tools.subtitle_remover_remote_call", + "backend.tools.video_io", + "backend.tools.ffmpeg_cli", + "backend.tools.process_manager", + "backend.tools.hardware_accelerator", + "backend.tools.inpaint_tools", + "backend.tools.common_tools", + "backend.config", + "backend.scenedetect", + "ui", + "ui.home_interface", + "ui.advanced_setting_interface", + "ui.setting_interface", + "ui.component", + "ui.component.video_display_component", + "ui.component.task_list_component", + "ui.icon.my_fluent_icon", + ] + + for imp in hidden_imports: + cmd += ["--hidden-import", imp] + + # 排除不需要的模块 + excludes = [ + "pytest", "unittest", "test", "tests", + "IPython", "jupyter", "notebook", "ipykernel", + "paddle.fluid.contrib", "paddle.fluid.dygraph", "paddle.fluid.optimizer", + "torch.utils.tensorboard", "torch.utils.bottleneck", + "sphinx", "docutils", "pandas", "matplotlib", "seaborn", + "scrapy", "beautifulsoup4", "lxml", "sklearn", "xgboost", "lightgbm", + ] + + for exc in excludes: + cmd += ["--exclude-module", exc] + + # CUDA 支持 + if include_cuda: + cmd += ["--collect-binaries", "nvidia"] + print("注意: 包含 NVIDIA CUDA 依赖") + + # 复制重要包的元数据(只复制确实存在的包) + important_packages = ["PySide6", "paddleocr", "torch", "torchvision", "numpy", "opencv-python", "scipy", "Pillow", "opencv-contrib-python"] + installed = get_installed_packages() + + for pkg in important_packages: + if pkg in installed: + try: + cmd += ["--copy-metadata", pkg] + except: + print(f"Warning: Could not copy metadata for {pkg}") + continue + + # UPX 压缩 + cmd += ["--upx-dir", "C:/upx"] if os.path.exists("C:/upx") else ["--noupx"] + + return cmd + +def main(): + parser = argparse.ArgumentParser(description="视频字幕去除器打包脚本") + parser.add_argument('--file', default='gui.py', help='主文件名,默认为 gui.py') + parser.add_argument('--nvidia', action='store_true', help='包含 NVIDIA CUDA 和 cuDNN 依赖') + parser.add_argument('--console', action='store_true', help='显示控制台窗口(用于调试)') + + args = parser.parse_args() + main_file = args.file + + if not os.path.exists(main_file): + print(f"错误: 找不到文件 {main_file}") + sys.exit(1) + + print("========================================") + print(" 视频字幕去除器 - PyInstaller 打包") + print("========================================") + print(f"主文件: {main_file}") + print(f"CUDA 支持: {'是' if args.nvidia else '否'}") + print(f"控制台: {'显示' if args.console else '隐藏'}") + print() + + # 构建命令 + cmd = build_package(main_file, include_cuda=args.nvidia) + + # 如果不显示控制台,添加 --noconsole + if not args.console: + cmd.append("--noconsole") + + print("PyInstaller 命令:") + print(" ".join(cmd)) + print() + print("开始打包,这可能需要几分钟...") + print() + + try: + # 执行打包,使用当前Python环境(vsr环境) + result = subprocess.run(cmd, check=True, env=os.environ.copy()) + print() + print("========================================") + print(" ✓ 打包成功完成!") + print("========================================") + print(f"输出目录: dist/VideoSubtitleRemover/") + print() + print("下一步:") + print("1. 测试运行: dist/VideoSubtitleRemover/VideoSubtitleRemover.exe") + print("2. 如果测试通过,可以删除 build/ 目录以节省空间") + return 0 + + except subprocess.CalledProcessError as e: + print() + print("========================================") + print(" ✗ 打包失败!") + print("========================================") + print(f"错误代码: {e.returncode}") + print() + print("故障排除:") + print("1. 检查是否所有依赖都已安装: pip list") + print("2. 查看详细错误日志: build/VideoSubtitleRemover/warn-VideoSubtitleRemover.txt") + print("3. 尝试使用 --console 参数查看详细错误信息") + return 1 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/build_windows.bat b/build_windows.bat new file mode 100644 index 0000000..7fdc9b8 --- /dev/null +++ b/build_windows.bat @@ -0,0 +1,115 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 视频字幕去除器 - PyInstaller 打包脚本 +echo ======================================== +echo. + +REM 读取版本号 +for /f "tokens=2 delims='" %%a in ('findstr /C:"VERSION = " backend\config.py') do set VERSION=%%a +echo 版本号: %VERSION% +echo. + +REM 检查是否安装了 PyInstaller +python -c "import PyInstaller" 2>nul +if errorlevel 1 ( + echo 错误: 未安装 PyInstaller + echo 正在安装 PyInstaller... + pip install pyinstaller + if errorlevel 1 ( + echo 安装失败,请手动运行: pip install pyinstaller + pause + exit /b 1 + ) +) + +REM 清理旧的构建文件 +echo [1/5] 清理旧的构建文件... +if exist build rmdir /s /q build +if exist dist rmdir /s /q dist +echo ✓ 清理完成 +echo. + +REM 执行 PyInstaller 打包 +echo [2/5] 开始 PyInstaller 打包... +echo 这可能需要几分钟时间,请耐心等待... +echo. +pyinstaller --clean --noconfirm VideoSubtitleRemover.spec +if errorlevel 1 ( + echo ✗ 打包失败! + pause + exit /b 1 +) +echo ✓ 打包完成 +echo. + +REM 重命名输出目录 +echo [3/5] 重命名输出目录... +set OUTPUT_NAME=VideoSubtitleRemover-Windows-v%VERSION% +if exist dist\%OUTPUT_NAME% rmdir /s /q dist\%OUTPUT_NAME% +move dist\VideoSubtitleRemover dist\%OUTPUT_NAME% +if errorlevel 1 ( + echo ✗ 重命名失败! + pause + exit /b 1 +) +echo ✓ 重命名完成: %OUTPUT_NAME% +echo. + +REM 检查是否安装了 7z +echo [4/5] 检查 7z 压缩工具... +where 7z >nul 2>&1 +if errorlevel 1 ( + echo 警告: 未找到 7z,跳过压缩步骤 + echo 请手动安装 7-Zip: https://www.7-zip.org/ + goto :skip_compression +) + +REM 创建 7z 压缩包 +echo 开始压缩(可能需要几分钟)... +cd dist\%OUTPUT_NAME% +7z a -t7z -mx=9 -m0=LZMA2 -ms=on -mfb=64 -md=32m -mmt=on -v2000m "..\vsr-v%VERSION%-windows-cpu.7z" * +cd ..\.. + +REM 检查是否只有一个分卷 +if exist "vsr-v%VERSION%-windows-cpu.7z.001" ( + if not exist "vsr-v%VERSION%-windows-cpu.7z.002" ( + rename "vsr-v%VERSION%-windows-cpu.7z.001" "vsr-v%VERSION%-windows-cpu.7z" + echo ✓ 压缩完成(单文件) + ) else ( + echo ✓ 压缩完成(分卷) + ) +) else if exist "vsr-v%VERSION%-windows-cpu.7z" ( + echo ✓ 压缩完成(单文件) +) else ( + echo ✗ 压缩失败! + goto :skip_compression +) + +:skip_compression +echo. + +REM 显示构建结果 +echo [5/5] 构建结果摘要 +echo ======================================== +echo 输出目录: dist\%OUTPUT_NAME% +echo. + +if exist "vsr-v%VERSION%-windows-cpu.7z" ( + echo 压缩包: vsr-v%VERSION%-windows-cpu.7z + for %%F in ("vsr-v%VERSION%-windows-cpu.7z") do echo 文件大小: %%~zF 字节 +) else if exist "vsr-v%VERSION%-windows-cpu.7z.001" ( + echo 压缩包: vsr-v%VERSION%-windows-cpu.7z.* (分卷) +) + +echo. +echo ======================================== +echo ✓ 构建成功完成! +echo ======================================== +echo. +echo 下一步操作: +echo 1. 测试运行: dist\%OUTPUT_NAME%\VideoSubtitleRemover.exe +echo 2. 分发压缩包(如果生成了) +echo. + +pause \ No newline at end of file diff --git a/gui.py b/gui.py index fffb63a..279b88e 100644 --- a/gui.py +++ b/gui.py @@ -41,8 +41,14 @@ class SubtitleExtractorGUI(FluentWindow): # self.themeListener = SystemThemeListener(self) # self.themeListener.start() - # 设置窗口图标 - self.setWindowIcon(QtGui.QIcon("design/vsr.ico")) + # 设置窗口图标(支持打包环境) + if getattr(sys, 'frozen', False): + # 打包后的环境 + icon_path = os.path.join(sys._MEIPASS, 'design', 'vsr.ico') + else: + # 开发环境 + icon_path = "design/vsr.ico" + self.setWindowIcon(QtGui.QIcon(icon_path)) self.setWindowTitle(tr['SubtitleExtractorGUI']['Title'] + " v" + VERSION) # 创建界面布局 self._create_layout() @@ -162,6 +168,8 @@ class SubtitleExtractorGUI(FluentWindow): if __name__ == '__main__': + # 支持打包后的多进程 + multiprocessing.freeze_support() multiprocessing.set_start_method("spawn") QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) diff --git a/ui/icon/my_fluent_icon.py b/ui/icon/my_fluent_icon.py index 6848506..7844fd5 100644 --- a/ui/icon/my_fluent_icon.py +++ b/ui/icon/my_fluent_icon.py @@ -1,3 +1,5 @@ +import os +import sys from enum import Enum from qfluentwidgets import getIconColor, Theme, FluentIconBase @@ -8,4 +10,12 @@ class MyFluentIcon(FluentIconBase, Enum): def path(self, theme=Theme.AUTO): # getIconColor() return "white" or "black" according to current theme - return f'./ui/icon/{self.value}_{getIconColor(theme)}.svg' + # 支持打包环境和开发环境 + if getattr(sys, 'frozen', False): + # 打包环境:使用 sys._MEIPASS 作为基础路径 + base_path = sys._MEIPASS + else: + # 开发环境:使用相对路径 + base_path = os.path.join(os.path.dirname(__file__), '..') + + return os.path.join(base_path, 'ui', 'icon', f'{self.value}_{getIconColor(theme)}.svg')