diff --git a/app/common/config.py b/app/common/config.py index 9ce62067..bb4f6cea 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -19,7 +19,7 @@ ) from app.config import SETTINGS_PATH, WORK_PATH -from app.core.utils.platform_utils import get_available_transcribe_models +from app.core.utils.platform_utils import get_available_transcribe_models, is_windows from ..core.entities import ( FasterWhisperModelEnum, @@ -174,10 +174,13 @@ class Config(QConfig): ) # ------------------- Faster Whisper 配置 ------------------- + _default_faster_whisper_program = ( + "faster-whisper-xxl.exe" if is_windows() else "faster-whisper-xxl" + ) faster_whisper_program = ConfigItem( "FasterWhisper", "Program", - "faster-whisper-xxl.exe", + _default_faster_whisper_program, ) faster_whisper_model = OptionsConfigItem( "FasterWhisper", diff --git a/app/components/FasterWhisperSettingWidget.py b/app/components/FasterWhisperSettingWidget.py index 55c1a2aa..d16f4f67 100644 --- a/app/components/FasterWhisperSettingWidget.py +++ b/app/components/FasterWhisperSettingWidget.py @@ -1,4 +1,6 @@ import os +import platform +import shutil import subprocess from pathlib import Path @@ -44,8 +46,7 @@ from app.thread.file_download_thread import FileDownloadThread from app.thread.modelscope_download_thread import ModelscopeDownloadThread -# 在文件开头添加常量定义 -FASTER_WHISPER_PROGRAMS = [ +WINDOWS_FASTER_WHISPER_PROGRAMS = [ { "label": "GPU(cuda) + CPU 版本", "value": "faster-whisper-gpu.7z", @@ -62,6 +63,21 @@ }, ] +LINUX_FASTER_WHISPER_PROGRAMS = [ + { + "label": "GPU(cuda) + CPU 版本", + "value": "Faster-Whisper-XXL_r245.4_linux.7z", + "type": "GPU", + "size": "1.30 GB", + "downloadLink": "https://github.com/Purfview/whisper-standalone-win/releases/download/Faster-Whisper-XXL/Faster-Whisper-XXL_r245.4_linux.7z", + } +] + +if platform.system() == "Linux": + FASTER_WHISPER_PROGRAMS = LINUX_FASTER_WHISPER_PROGRAMS +else: + FASTER_WHISPER_PROGRAMS = WINDOWS_FASTER_WHISPER_PROGRAMS + FASTER_WHISPER_MODELS = [ { "label": "Tiny", @@ -127,8 +143,8 @@ def check_faster_whisper_exists() -> tuple[bool, list[str]]: """检查 faster-whisper 程序是否存在 检查以下两种情况: - 1. bin目录下是否有 faster-whisper.exe - 2. bin目录下是否有 Faster-Whisper-XXL/faster-whisper-xxl.exe + 1. PATH/本地目录是否有 faster-whisper(.exe) + 2. PATH/本地目录是否有 faster-whisper-xxl(.exe) Returns: tuple[bool, list[str]]: (是否存在程序, 已安装的版本列表) @@ -136,15 +152,28 @@ def check_faster_whisper_exists() -> tuple[bool, list[str]]: bin_path = Path(BIN_PATH) installed_versions = [] - # 检查 faster-whisper.exe(CPU版本) - if (bin_path / "faster-whisper.exe").exists(): + # 检查 faster-whisper(CPU版本) + cpu_candidates = [ + bin_path / "faster-whisper", + bin_path / "faster-whisper.exe", + ] + has_cpu = any(p.exists() for p in cpu_candidates) or bool(shutil.which("faster-whisper")) + if has_cpu: installed_versions.append("CPU") - # 检查 Faster-Whisper-XXL/faster-whisper-xxl.exe(GPU版本) - xxl_path = bin_path / "Faster-Whisper-XXL" / "faster-whisper-xxl.exe" - if xxl_path.exists(): + # 检查 faster-whisper-xxl(GPU版本) + xxl_candidates = [ + bin_path / "faster-whisper-xxl", + bin_path / "faster-whisper-xxl.exe", + bin_path / "Faster-Whisper-XXL" / "faster-whisper-xxl", + bin_path / "Faster-Whisper-XXL" / "faster-whisper-xxl.exe", + ] + has_xxl = any(p.exists() for p in xxl_candidates) or bool( + shutil.which("faster-whisper-xxl") + ) + if has_xxl: installed_versions.extend(["GPU", "CPU"]) - installed_versions = list(set(installed_versions)) + installed_versions = sorted(set(installed_versions)) return bool(installed_versions), installed_versions @@ -457,7 +486,7 @@ def _on_program_download_finished(self, save_path): """程序下载完成处理""" try: # 检查是否是 CPU 版本的直接下载 - if save_path.endswith(".exe"): + if save_path.lower().endswith(".exe"): # 如果是exe文件,重命名为faster-whisper.exe os.rename(save_path, os.path.join(BIN_PATH, "faster-whisper.exe")) self._finish_program_installation() @@ -633,6 +662,17 @@ def _open_program_folder(self): def _finish_program_installation(self): """完成程序安装""" + # Linux 可执行文件下载后需要补充执行权限 + if platform.system() == "Linux": + for candidate in [ + Path(BIN_PATH) / "faster-whisper-xxl", + Path(BIN_PATH) / "faster-whisper", + Path(BIN_PATH) / "Faster-Whisper-XXL" / "faster-whisper-xxl", + Path(BIN_PATH) / "Faster-Whisper-XXL" / "faster-whisper", + ]: + if candidate.exists(): + candidate.chmod(candidate.stat().st_mode | 0o111) + InfoBar.success( self.tr("安装完成"), self.tr("Faster Whisper 程序已安装成功"), diff --git a/app/core/asr/faster_whisper.py b/app/core/asr/faster_whisper.py index c9ec8b74..4ecffd17 100644 --- a/app/core/asr/faster_whisper.py +++ b/app/core/asr/faster_whisper.py @@ -9,6 +9,7 @@ import GPUtil +from ...config import BIN_PATH from ..utils.logger import setup_logger from ..utils.subprocess_helper import StreamReader from .asr_data import ASRData, ASRDataSeg @@ -96,21 +97,115 @@ def __init__( self.one_word = 0 self.sentence = True - # 根据设备选择程序 - if self.device == "cpu": - if shutil.which("faster-whisper-xxl"): - self.faster_whisper_program = "faster-whisper-xxl" - else: - if not shutil.which("faster-whisper"): - raise EnvironmentError("faster-whisper程序未找到,请确保已经下载。") - self.faster_whisper_program = "faster-whisper" - self.vad_method = "" - elif self.device == "cuda": - if not shutil.which("faster-whisper-xxl"): - raise EnvironmentError( - "faster-whisper-xxl 程序未找到,请确保已经下载。" - ) - self.faster_whisper_program = "faster-whisper-xxl" + # 根据设备和平台自动解析程序路径(支持 Linux/Windows、本地 bin 目录和 PATH) + self.faster_whisper_program = self._resolve_program( + preferred_program=faster_whisper_program, + device=self.device, + ) + + # CPU 下如果退化到普通 faster-whisper,关闭 xxl 才支持的 VAD 方法参数 + if self.device == "cpu" and not self._is_xxl_program(self.faster_whisper_program): + self.vad_method = "" + + @staticmethod + def _is_xxl_program(program: str) -> bool: + return "faster-whisper-xxl" in Path(program).name.lower() + + @staticmethod + def _is_error_line(line: str) -> bool: + lower_line = line.lower() + return ( + "error" in lower_line + or "failed" in lower_line + or "traceback" in lower_line + or "exception" in lower_line + ) + + @staticmethod + def _detect_glibc_error(line: str) -> bool: + lower_line = line.lower() + return "glibc_" in lower_line and "not found" in lower_line + + @staticmethod + def _candidate_paths(name: str) -> List[Path]: + """Generate local filesystem candidates from a command/program name.""" + if not name: + return [] + + candidate = Path(name) + paths: List[Path] = [] + + # Absolute or relative path from config/user + if candidate.is_absolute() or candidate.parent != Path("."): + paths.append(candidate) + if not str(candidate).lower().endswith(".exe"): + paths.append(Path(f"{candidate}.exe")) + + # Common local locations in this project + local_names = [name] + if not name.lower().endswith(".exe"): + local_names.append(f"{name}.exe") + + for local_name in local_names: + paths.append(BIN_PATH / local_name) + paths.append(BIN_PATH / "Faster-Whisper-XXL" / local_name) + + return paths + + @classmethod + def _resolve_existing_program(cls, name: str) -> Optional[str]: + """Resolve executable by checking PATH and local bin folders.""" + if not name: + return None + + # PATH lookup first + which_path = shutil.which(name) + if which_path: + return which_path + + # Also try .exe on non-Windows when config keeps old value + if not name.lower().endswith(".exe"): + which_exe = shutil.which(f"{name}.exe") + if which_exe: + return which_exe + + for candidate in cls._candidate_paths(name): + if candidate.exists(): + return str(candidate) + + return None + + @classmethod + def _resolve_program(cls, preferred_program: str, device: str) -> str: + """Resolve usable faster-whisper executable path for target device.""" + if device == "cuda": + candidate_names = [ + preferred_program, + "faster-whisper-xxl", + "faster-whisper-xxl.exe", + ] + else: + candidate_names = [ + preferred_program, + "faster-whisper-xxl", + "faster-whisper-xxl.exe", + "faster-whisper", + "faster-whisper.exe", + ] + + # Keep order while removing empty/duplicated values + deduped_names = list(dict.fromkeys([name for name in candidate_names if name])) + + for name in deduped_names: + resolved = cls._resolve_existing_program(name) + if resolved: + return resolved + + if device == "cuda": + raise EnvironmentError( + "faster-whisper-xxl 程序未找到,请先下载 Linux/Windows 对应版本。" + ) + raise EnvironmentError("faster-whisper 程序未找到,请确保已经下载。") def _build_command(self, audio_input: str) -> List[str]: """Build command line arguments for faster-whisper.""" @@ -155,9 +250,7 @@ def _build_command(self, audio_input: str) -> List[str]: cmd.extend(["--vad_filter", "false"]) # 人声分离 - if self.ff_mdx_kim2 and self.faster_whisper_program.startswith( - "faster-whisper-xxl" - ): + if self.ff_mdx_kim2 and self._is_xxl_program(self.faster_whisper_program): cmd.append("--ff_mdx_kim2") # 文本处理参数 @@ -264,6 +357,7 @@ def _default_callback(x, y): is_finish = False error_msg = "" + known_runtime_error = "" last_progress = 0 # 实时处理输出 @@ -274,8 +368,14 @@ def _default_callback(x, y): for stream_name, line in reader.get_remaining_output(): line = line.strip() if line: - if "error" in line: - error_msg += line + if self._is_error_line(line): + error_msg += f"{line}\n" + if self._detect_glibc_error(line): + known_runtime_error = ( + "Faster-Whisper-XXL 与当前系统 GLIBC 不兼容。" + "请重新运行 run.sh 自动切换兼容版本," + "或在环境变量 FASTER_WHISPER_XXL_LINUX_URL 指定兼容包。" + ) else: logger.info(line) break @@ -299,15 +399,23 @@ def _default_callback(x, y): if "Subtitles are written to" in line: is_finish = True callback(*ASRStatus.COMPLETED.callback_tuple()) - if "error" in line or "Error" in line: - error_msg += line + if self._is_error_line(line): + error_msg += f"{line}\n" + if self._detect_glibc_error(line): + known_runtime_error = ( + "Faster-Whisper-XXL 与当前系统 GLIBC 不兼容。" + "请重新运行 run.sh 自动切换兼容版本," + "或在环境变量 FASTER_WHISPER_XXL_LINUX_URL 指定兼容包。" + ) logger.error(line) else: logger.info(line) if not is_finish: logger.error("Faster Whisper 错误: %s", error_msg) - raise RuntimeError(error_msg) + if known_runtime_error: + raise RuntimeError(known_runtime_error) + raise RuntimeError(error_msg.strip() or "Faster Whisper 运行失败") # 判断是否识别成功 if not output_path.exists(): diff --git a/scripts/run.sh b/scripts/run.sh old mode 100644 new mode 100755 index 3ef0f316..f046f2b2 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -7,6 +7,10 @@ set -e # Configuration REPO_URL="https://github.com/WEIFENG2333/VideoCaptioner.git" INSTALL_DIR="${VIDEOCAPTIONER_HOME:-$HOME/VideoCaptioner}" +FASTER_WHISPER_XXL_LINUX_URL_MODERN="https://github.com/Purfview/whisper-standalone-win/releases/download/Faster-Whisper-XXL/Faster-Whisper-XXL_r245.4_linux.7z" +FASTER_WHISPER_XXL_LINUX_URL_LEGACY="https://github.com/Purfview/whisper-standalone-win/releases/download/Faster-Whisper-XXL/Faster-Whisper-XXL_r192.3.1_linux.7z" +FASTER_WHISPER_XXL_GLIBC_MIN="2.35" +FASTER_WHISPER_XXL_LINUX_URL="${FASTER_WHISPER_XXL_LINUX_URL:-}" # Colors for output RED='\033[0;31m' @@ -20,6 +24,175 @@ print_success() { echo -e "${GREEN}[OK]${NC} $1"; } print_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } +version_lt() { + local v1="$1" + local v2="$2" + [ "$(printf '%s\n%s\n' "$v1" "$v2" | sort -V | head -n 1)" = "$v1" ] && [ "$v1" != "$v2" ] +} + +get_glibc_version() { + ldd --version 2>/dev/null | awk 'NR==1{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+\.[0-9]+$/){print $i; exit}}' +} + +resolve_faster_whisper_xxl_linux_url() { + # 用户显式指定下载源时,优先使用 + if [ -n "$FASTER_WHISPER_XXL_LINUX_URL" ]; then + echo "$FASTER_WHISPER_XXL_LINUX_URL" + return 0 + fi + + local glibc_version + glibc_version="$(get_glibc_version)" + + # glibc 低于 2.35 时,使用 legacy 版本避免 GLIBC_2.35 not found + if [ -n "$glibc_version" ] && version_lt "$glibc_version" "$FASTER_WHISPER_XXL_GLIBC_MIN"; then + print_warning "Detected GLIBC $glibc_version (< $FASTER_WHISPER_XXL_GLIBC_MIN), using legacy XXL package." >&2 + echo "$FASTER_WHISPER_XXL_LINUX_URL_LEGACY" + return 0 + fi + + echo "$FASTER_WHISPER_XXL_LINUX_URL_MODERN" +} + +xxl_binary_usable() { + local binary="$1" + [ -x "$binary" ] || return 1 + + local output + output="$("$binary" --help 2>&1 || true)" + + if echo "$output" | grep -qiE 'GLIBC_[0-9]+\.[0-9]+.*not found'; then + return 1 + fi + + return 0 +} + +download_file() { + local url="$1" + local output="$2" + + if command -v curl &> /dev/null; then + curl -L --fail --retry 2 --connect-timeout 10 -o "$output" "$url" + elif command -v wget &> /dev/null; then + wget -O "$output" "$url" + else + print_error "Neither curl nor wget found. Please install one of them first." + return 1 + fi +} + +extract_7z() { + local archive="$1" + local output_dir="$2" + + if command -v 7z &> /dev/null; then + 7z x "$archive" "-o$output_dir" -y >/dev/null + return 0 + fi + + if command -v 7zz &> /dev/null; then + 7zz x "$archive" "-o$output_dir" -y >/dev/null + return 0 + fi + + print_warning "7z not found. Using Python py7zr fallback via uv..." + uv run --with py7zr python - "$archive" "$output_dir" <<'PY' +import pathlib +import sys + +import py7zr + +archive = pathlib.Path(sys.argv[1]) +output_dir = pathlib.Path(sys.argv[2]) +output_dir.mkdir(parents=True, exist_ok=True) + +with py7zr.SevenZipFile(archive, mode="r") as zf: + zf.extractall(path=output_dir) +PY +} + +install_faster_whisper_xxl_linux() { + if [[ "$OSTYPE" != "linux-gnu"* ]]; then + return 0 + fi + + local bin_dir="$INSTALL_DIR/resource/bin" + local xxl_dir="$bin_dir/Faster-Whisper-XXL" + local xxl_bin="$xxl_dir/faster-whisper-xxl" + local xxl_link="$bin_dir/faster-whisper-xxl" + local selected_url + selected_url="$(resolve_faster_whisper_xxl_linux_url)" + local archive_path="$bin_dir/$(basename "$selected_url")" + local need_install=1 + + mkdir -p "$bin_dir" + + # 已经存在且可运行:不下载、不解压 + if [ -f "$xxl_bin" ]; then + chmod +x "$xxl_bin" + ln -sf "$xxl_bin" "$xxl_link" + if xxl_binary_usable "$xxl_bin"; then + print_success "Faster-Whisper-XXL already installed: $xxl_bin" + need_install=0 + else + print_warning "Existing binary is incompatible with current runtime, will reinstall: $xxl_bin" + fi + elif [ -x "$xxl_link" ]; then + if xxl_binary_usable "$xxl_link"; then + print_success "Faster-Whisper-XXL already installed: $xxl_link" + need_install=0 + else + print_warning "Existing binary is incompatible with current runtime, will reinstall: $xxl_link" + fi + fi + + if [ "$need_install" -eq 1 ]; then + print_info "Installing Faster-Whisper-XXL (Linux)..." + + if [ -f "$archive_path" ]; then + print_info "Found existing archive, skip download: $archive_path" + else + print_info "Download source: $selected_url" + print_info "Archive path: $archive_path" + download_file "$selected_url" "$archive_path" + fi + + print_info "Extracting archive..." + extract_7z "$archive_path" "$bin_dir" + + if [ ! -f "$xxl_bin" ]; then + local detected_bin + detected_bin="$(find "$bin_dir" -maxdepth 4 -type f -name "faster-whisper-xxl" | head -n 1 || true)" + if [ -n "$detected_bin" ]; then + mkdir -p "$xxl_dir" + cp "$detected_bin" "$xxl_bin" + fi + fi + + if [ ! -f "$xxl_bin" ]; then + print_error "Faster-Whisper-XXL install failed: binary not found after extraction." + print_error "Expected path: $xxl_bin" + exit 1 + fi + + chmod +x "$xxl_bin" + ln -sf "$xxl_bin" "$xxl_link" + + if ! xxl_binary_usable "$xxl_bin"; then + local glibc_version + glibc_version="$(get_glibc_version)" + print_error "Installed XXL binary is still not runnable on this system (GLIBC: ${glibc_version:-unknown})." + print_error "Please set a compatible package URL via FASTER_WHISPER_XXL_LINUX_URL and rerun." + exit 1 + fi + + print_success "Faster-Whisper-XXL installed: $xxl_bin" + fi + + export PATH="$xxl_dir:$bin_dir:$PATH" +} + # Check if running from within the project directory detect_project_dir() { # If main.py exists in current directory, use it @@ -166,9 +339,11 @@ main() { # Check system dependencies check_system_deps + # Install Faster-Whisper-XXL (Linux) + install_faster_whisper_xxl_linux + # Run the app run_app } main "$@" -