添加PyInstaller打包支持和运行时路径修复

- 创建PyInstaller规范文件和打包脚本
- 修复开发/打包环境路径兼容性问题
- 添加PaddleOCR运行时依赖(opencv-contrib-python, pypdfium2, pyclipper)
- 支持打包后的多进程启动
- 修复图标路径和翻译文件路径
- 清理重复的模型和FFmpeg文件
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yaofanguk
2026-04-10 19:23:05 +08:00
parent 293cd9bbee
commit c80e7fabc9
18 changed files with 423 additions and 33 deletions

View File

@@ -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'

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +0,0 @@
filename,filesize,encoding,header
ffmpeg_1.exe,50000000,,
ffmpeg_2.exe,50000000,,
ffmpeg_3.exe,13721856,,
1 filename filesize encoding header
2 ffmpeg_1.exe 50000000
3 ffmpeg_2.exe 50000000
4 ffmpeg_3.exe 13721856

View File

@@ -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,,
1 filename filesize encoding header
2 big-lama_1.pt 50000000
3 big-lama_2.pt 50000000
4 big-lama_3.pt 50000000
5 big-lama_4.pt 50000000
6 big-lama_5.pt 5803670

View File

@@ -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':

View File

@@ -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')
return os.path.join(base_path, 'ffmpeg', 'macos', 'ffmpeg')

View File

@@ -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}"
)

222
build_pinstaller.py Normal file
View File

@@ -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())

115
build_windows.bat Normal file
View File

@@ -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

12
gui.py
View File

@@ -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)

View File

@@ -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')