From 25782333e20f6108214086775f82e9e213a124f8 Mon Sep 17 00:00:00 2001 From: stirelshka8 Date: Tue, 24 Mar 2026 18:34:32 +0300 Subject: [PATCH] uppd --- core/video_processor.py | 82 +++++++++++++++++++++++++++++++++++++++-- gui/main_window.py | 11 ++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/core/video_processor.py b/core/video_processor.py index 31b6edb..f42969c 100644 --- a/core/video_processor.py +++ b/core/video_processor.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess import sys import tempfile @@ -126,6 +127,11 @@ class VideoProcessor: def __init__(self, logger=None): self.logger = logger or Logger() + def check_disk_space_for_job(self, output_dir, video_data, quality): + """Публичная проверка места на диске до старта обработки.""" + use_batch = len(video_data) > BATCH_THRESHOLD + self._check_disk_space_or_raise(output_dir, video_data, quality, use_batch) + def process_videos(self, video_data, output_dir, output_format="mp4", quality="high", progress_callback=None, fps=None, preset="medium", audio_bitrate="192k", @@ -138,6 +144,7 @@ class VideoProcessor: check = cancel_check if callable(cancel_check) else lambda: False use_batch = len(video_data) > BATCH_THRESHOLD + self._check_disk_space_or_raise(output_dir, video_data, quality, use_batch) if use_batch: self.logger.log("INFO", f"Режим пакетной обработки (файлов > {BATCH_THRESHOLD}), экономия памяти") return self._process_videos_batch( @@ -254,7 +261,9 @@ class VideoProcessor: try: ensure_directory(output_dir) - with tempfile.TemporaryDirectory(prefix="ave_") as tmpdir: + # Временные части создаём рядом с выходным каталогом: + # это снижает риск переполнения системного диска (обычно C:\Temp). + with tempfile.TemporaryDirectory(prefix="ave_", dir=output_dir) as tmpdir: for i, data in enumerate(video_data): if cancel_check(): raise CancelledError() @@ -390,9 +399,9 @@ class VideoProcessor: except CancelledError: self._remove_if_exists(output_path) raise - except Exception: + except Exception as e: self._remove_if_exists(output_path) - raise + raise RuntimeError(self._friendly_write_error(e, output_dir)) from e if resized is not None: try: resized.close() @@ -416,6 +425,73 @@ class VideoProcessor: except OSError: pass + @staticmethod + def _friendly_write_error(error, output_dir): + msg = str(error or "") + low = msg.lower() + if "no space left on device" in low or "errno 32" in low or "broken pipe" in low: + return ( + "Недостаточно свободного места на диске для сохранения видео. " + f"Освободите место в папке назначения ({output_dir}) и повторите попытку." + ) + return msg + + def _check_disk_space_or_raise(self, output_dir, video_data, quality, use_batch): + try: + free_bytes = shutil.disk_usage(output_dir).free + except Exception: + # Если не получилось определить свободное место, не блокируем обработку. + return + + required_bytes = self._estimate_required_space_bytes(video_data, quality, use_batch) + if free_bytes >= required_bytes: + self.logger.log( + "INFO", + f"Проверка места: требуется ~{self._format_size(required_bytes)}, доступно {self._format_size(free_bytes)}" + ) + return + + raise RuntimeError( + "Недостаточно свободного места для обработки. " + f"Нужно примерно {self._format_size(required_bytes)}, доступно {self._format_size(free_bytes)}. " + "Выберите другой диск или освободите место." + ) + + @staticmethod + def _estimate_required_space_bytes(video_data, quality, use_batch): + input_total = 0 + for data in video_data: + path = data.get("path") + if path and os.path.isfile(path): + try: + input_total += os.path.getsize(path) + except OSError: + pass + + # Грубая оценка размера результата с запасом в зависимости от качества. + quality_factor = { + "low": 0.7, + "medium": 1.0, + "high": 1.4, + }.get(quality, 1.0) + output_estimate = int(input_total * quality_factor) + + # В пакетном режиме временные части могут занимать объём, близкий к итогу. + temp_estimate = output_estimate if use_batch else int(output_estimate * 0.2) + + # Дополнительный запас на контейнер/служебные файлы и колебания битрейта. + safety_margin = int((output_estimate + temp_estimate) * 0.25) + return output_estimate + temp_estimate + safety_margin + + @staticmethod + def _format_size(num_bytes): + units = ["B", "KB", "MB", "GB", "TB"] + value = float(max(0, num_bytes)) + for unit in units: + if value < 1024 or unit == units[-1]: + return f"{value:.1f} {unit}" + value /= 1024.0 + def get_bitrate(self, quality): """Определение битрейта в зависимости от качества""" quality_settings = { diff --git a/gui/main_window.py b/gui/main_window.py index 051942d..9e4500e 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -763,6 +763,17 @@ Anime Video Editor — объединение нескольких видео в cancel_event = threading.Event() self._cancel_event = cancel_event + # Проверяем место сразу по нажатию "Выполнить", до запуска фоновой обработки. + try: + VideoProcessor(logger=logger).check_disk_space_for_job( + output_dir=output_dir, + video_data=video_data, + quality=quality, + ) + except Exception as e: + messagebox.showerror("Недостаточно места", str(e)) + return + def worker(): try: processor = VideoProcessor(logger=logger)