diff --git a/videocaptioner/core/asr/faster_whisper.py b/videocaptioner/core/asr/faster_whisper.py index d34f6bcb..3dc037b8 100644 --- a/videocaptioner/core/asr/faster_whisper.py +++ b/videocaptioner/core/asr/faster_whisper.py @@ -13,6 +13,7 @@ GPUtil = None # type: ignore[assignment] from ..utils.logger import setup_logger +from ..utils.platform_utils import get_subprocess_kwargs from ..utils.subprocess_helper import StreamReader from .asr_data import ASRData, ASRDataSeg from .base import BaseASR @@ -258,7 +259,7 @@ def _default_callback(x, y): text=True, encoding="utf-8", errors="ignore", - creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 使用 StreamReader 处理输出 diff --git a/videocaptioner/core/asr/whisper_cpp.py b/videocaptioner/core/asr/whisper_cpp.py index edc26367..779a42f4 100644 --- a/videocaptioner/core/asr/whisper_cpp.py +++ b/videocaptioner/core/asr/whisper_cpp.py @@ -10,6 +10,7 @@ from ...config import MODEL_PATH from ..utils.logger import setup_logger +from ..utils.platform_utils import get_subprocess_kwargs from ..utils.subprocess_helper import StreamReader from .asr_data import ASRData, ASRDataSeg from .base import BaseASR @@ -155,6 +156,7 @@ def _default_callback(_progress: int, _message: str) -> None: text=True, encoding="utf-8", bufsize=1, + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) logger.debug(f"Whisper.cpp process started, PID: {self.process.pid}") @@ -247,7 +249,7 @@ def get_audio_duration(self, filepath: str) -> int: text=True, encoding="utf-8", errors="replace", - creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) info = result.stderr if duration_match := re.search(r"Duration: (\d+):(\d+):(\d+\.\d+)", info): diff --git a/videocaptioner/core/subtitle/ass_renderer.py b/videocaptioner/core/subtitle/ass_renderer.py index 608003d0..2fd85d7b 100644 --- a/videocaptioner/core/subtitle/ass_renderer.py +++ b/videocaptioner/core/subtitle/ass_renderer.py @@ -12,6 +12,7 @@ from videocaptioner.config import CACHE_PATH, FONTS_PATH, RESOURCE_PATH from videocaptioner.core.entities import SubtitleLayoutEnum from videocaptioner.core.utils.logger import setup_logger +from videocaptioner.core.utils.platform_utils import get_subprocess_kwargs from .ass_utils import auto_wrap_ass_file @@ -172,11 +173,7 @@ def render_ass_preview( str(default_bg), ], capture_output=True, - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) - if os.name == "nt" - else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) bg_path_obj = default_bg @@ -205,9 +202,7 @@ def render_ass_preview( result = subprocess.run( cmd, capture_output=True, - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) if result.returncode != 0: @@ -228,9 +223,7 @@ def _get_video_resolution(video_path: str) -> Tuple[int, int]: ["ffmpeg", "-i", video_path], capture_output=True, text=True, - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 从 ffmpeg 输出中解析分辨率 @@ -350,9 +343,7 @@ def render_ass_video( text=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 实时Reading输出并调用回调 diff --git a/videocaptioner/core/subtitle/rounded_renderer.py b/videocaptioner/core/subtitle/rounded_renderer.py index b9fda706..2212cde6 100644 --- a/videocaptioner/core/subtitle/rounded_renderer.py +++ b/videocaptioner/core/subtitle/rounded_renderer.py @@ -12,6 +12,7 @@ from videocaptioner.core.entities import SubtitleLayoutEnum from videocaptioner.core.utils.logger import setup_logger +from videocaptioner.core.utils.platform_utils import get_subprocess_kwargs from .font_utils import FontType, get_font from .styles import RoundedBgStyle @@ -31,7 +32,7 @@ def _get_video_info(video_path: str) -> Tuple[int, int, float]: text=True, encoding="utf-8", errors="replace", - creationflags=(getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 解析分辨率 @@ -442,9 +443,7 @@ def render_rounded_video( text=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) if result.returncode != 0: diff --git a/videocaptioner/core/utils/platform_utils.py b/videocaptioner/core/utils/platform_utils.py index 4b6383e6..ca405bec 100644 --- a/videocaptioner/core/utils/platform_utils.py +++ b/videocaptioner/core/utils/platform_utils.py @@ -83,16 +83,36 @@ def get_subprocess_kwargs(): """ 获取跨平台的subprocess参数 + 在 Windows GUI 应用中,子进程(如 ffmpeg、ASR 解码器等)启动时会弹出控制台窗口, + 这会干扰用户体验。本函数返回 Windows 下用于隐藏控制台窗口的参数: + - creationflags = CREATE_NO_WINDOW:最常用的隐藏方式 + - startupinfo.wShowWindow = SW_HIDE:备用方案,确保窗口完全不可见 + + 非 Windows 系统返回空字典,不影响正常行为。 + + Usage: + subprocess.run(cmd, **get_subprocess_kwargs()) + subprocess.Popen(cmd, **get_subprocess_kwargs()) + Returns: - dict: subprocess参数字典 + dict: subprocess参数字典,Windows 下包含 creationflags/startupinfo,其他系统为空 """ kwargs = {} - # 仅在Windows上添加CREATE_NO_WINDOW标志 + # 仅在Windows上添加静默启动参数 if platform.system() == "Windows": if hasattr(subprocess, "CREATE_NO_WINDOW"): kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if hasattr(subprocess, "STARTUPINFO") and hasattr( + subprocess, "STARTF_USESHOWWINDOW" + ): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + if hasattr(subprocess, "SW_HIDE"): + startupinfo.wShowWindow = subprocess.SW_HIDE + kwargs["startupinfo"] = startupinfo + return kwargs diff --git a/videocaptioner/core/utils/video_utils.py b/videocaptioner/core/utils/video_utils.py index 2474a96f..d5a664db 100644 --- a/videocaptioner/core/utils/video_utils.py +++ b/videocaptioner/core/utils/video_utils.py @@ -17,6 +17,7 @@ from ..subtitle.ass_utils import auto_wrap_ass_file from ..subtitle.rounded_renderer import render_rounded_video from ..utils.logger import setup_logger +from ..utils.platform_utils import get_subprocess_kwargs if TYPE_CHECKING: from videocaptioner.core.asr.asr_data import ASRData @@ -104,9 +105,7 @@ def video2audio(input_file: str, output: str = "", audio_track_index: int = 0) - check=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) if result.returncode == 0 and Path(output).is_file(): logger.debug("Audio conversion complete") @@ -136,9 +135,7 @@ def check_cuda_available() -> bool: ["ffmpeg", "-hwaccels"], capture_output=True, text=True, - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) if "cuda" not in result.stdout.lower(): logger.debug("CUDA not in FFmpeg hwaccels list") @@ -149,9 +146,7 @@ def check_cuda_available() -> bool: ["ffmpeg", "-hide_banner", "-init_hw_device", "cuda"], capture_output=True, text=True, - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 如果stderr中包含"Cannot load cuda" 或 "Failed to load"等Error output,说明CUDA不可用 @@ -232,11 +227,7 @@ def add_subtitles( text=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) - if os.name == "nt" - else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) logger.debug("Soft subtitle added") except subprocess.CalledProcessError as e: @@ -301,11 +292,7 @@ def add_subtitles( text=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) - if os.name == "nt" - else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 实时Reading输出并调用回调函数 @@ -390,9 +377,7 @@ def get_video_info( text=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) info = result.stderr @@ -514,9 +499,7 @@ def _extract_thumbnail(video_path: str, seek_time: float, thumbnail_path: str) - text=True, encoding="utf-8", errors="replace", - creationflags=( - getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0 - ), + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) return result.returncode == 0 diff --git a/videocaptioner/ui/components/FasterWhisperSettingWidget.py b/videocaptioner/ui/components/FasterWhisperSettingWidget.py index dca7ea28..a3553c51 100644 --- a/videocaptioner/ui/components/FasterWhisperSettingWidget.py +++ b/videocaptioner/ui/components/FasterWhisperSettingWidget.py @@ -37,7 +37,7 @@ TranscribeLanguageEnum, VadMethodEnum, ) -from videocaptioner.core.utils.platform_utils import open_folder +from videocaptioner.core.utils.platform_utils import get_subprocess_kwargs, open_folder from videocaptioner.ui.common.config import cfg from videocaptioner.ui.components.LineEditSettingCard import LineEditSettingCard from videocaptioner.ui.components.SpinBoxSettingCard import DoubleSpinBoxSettingCard @@ -166,7 +166,7 @@ def run(self): subprocess.run( ["7z", "x", self.zip_file, f"-o{self.extract_path}", "-y"], check=True, - creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + **get_subprocess_kwargs(), # Windows 隐藏控制台窗口 ) # 删除压缩包 os.remove(self.zip_file) diff --git a/videocaptioner/ui/view/batch_process_interface.py b/videocaptioner/ui/view/batch_process_interface.py index 121d6a83..865af932 100644 --- a/videocaptioner/ui/view/batch_process_interface.py +++ b/videocaptioner/ui/view/batch_process_interface.py @@ -50,7 +50,10 @@ def __init__(self, parent=None): super().__init__(parent=parent) self.setObjectName("batchProcessInterface") self.setWindowTitle(self.tr("批量处理")) - self.setAcceptDrops(True) + + # 由 MainWindow 统一处理拖放,避免子界面抢占拖放目标 + self.setAcceptDrops(False) + self.batch_thread = BatchProcessThread() self.init_ui() @@ -163,16 +166,6 @@ def on_add_file_clicked(self): if files: self.add_files(files) - def dragEnterEvent(self, event): - if event.mimeData().hasUrls(): - event.accept() - else: - event.ignore() - - def dropEvent(self, event): - files = [url.toLocalFile() for url in event.mimeData().urls()] - self.add_files(files) - def add_files(self, file_paths): task_type = BatchTaskType(self.task_type_combo.currentText()) diff --git a/videocaptioner/ui/view/main_window.py b/videocaptioner/ui/view/main_window.py index 8804e584..beb4834c 100644 --- a/videocaptioner/ui/view/main_window.py +++ b/videocaptioner/ui/view/main_window.py @@ -62,6 +62,9 @@ def __init__(self): # 注册退出处理, 清理进程 atexit.register(self.stop) + # 启用拖放支持 + self.setAcceptDrops(True) + def initNavigation(self): """初始化导航栏""" # 添加导航项 @@ -119,6 +122,45 @@ def initWindow(self): self.show() QApplication.processEvents() + def _get_drop_target(self): + """获取当前可接收拖放文件的子界面""" + current = self.stackedWidget.currentWidget() + if not current or not hasattr(current, "add_files") or not current.isEnabled(): + return None + return current + + def dragEnterEvent(self, event): + """窗口拖放进入事件,仅在当前页面可处理时接受""" + if not event.mimeData().hasUrls(): + event.ignore() + return + + current = self._get_drop_target() + if current is None: + event.ignore() + return + + files = [url.toLocalFile() for url in event.mimeData().urls() if url.isLocalFile()] + if files: + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event): + """窗口拖放放下事件,转发文件给当前子界面处理""" + current = self._get_drop_target() + if current is None: + event.ignore() + return + + files = [url.toLocalFile() for url in event.mimeData().urls() if url.isLocalFile()] + if not files: + event.ignore() + return + + current.add_files(files) + event.acceptProposedAction() + def onGithubDialog(self): """打开GitHub""" w = MessageBox(