0
0
forked from flt/tools

VideoCompress

This commit is contained in:
2025-07-25 14:08:06 +08:00
parent d941ea3341
commit 0acfaa1429
7 changed files with 6757 additions and 10 deletions

12
VideoCompress/config.json Normal file
View File

@ -0,0 +1,12 @@
{
"save_to": "single",
"crf": 18,
"codec": "h264",
"ffmpeg": "ffmpeg",
"video_ext": [
".mp4",
".mkv"
],
"extra": [],
"train": false
}

View File

@ -6,9 +6,14 @@ from tkinter import ttk, messagebox, filedialog
import main as main_program
from pathlib import Path
CONFIG_NAME = Path(sys.path[0])/"config.json"
if os.environ.get("INSTALL","0") == "1":
CONFIG_NAME = Path(os.getenv("APP_DATA", "C:/"))/"VideoCompress"/"config.json"
CONFIG_NAME.parent.mkdir(parents=True, exist_ok=True)
else:
CONFIG_NAME = Path(sys.path[0])/"config.json"
DEFAULT_CONFIG = {
"save_to": "multi",
"crf": 18,
"codec": "h264", # could be h264, h264_qsv, h264_nvenc … etc.
"ffmpeg": "ffmpeg",
@ -20,6 +25,7 @@ DEFAULT_CONFIG = {
HW_SUFFIXES = ["amf", "qsv", "nvenc"]
CODECS_BASE = ["h264", "hevc"]
SAVE_METHOD = ["保存到单一Compress", "每个文件夹下建立Compress"]
preset_options = {
"不使用":["","ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow",],
"AMD": ["","speed","balanced","quality",],
@ -95,7 +101,7 @@ class ConfigApp(tk.Tk):
return var
def _list_var_entry(self, key: str, width: int = 28):
"""Commaseparated list entry bound to config[key]."""
"""Comma-separated list entry bound to config[key]."""
var = tk.StringVar(value=",".join(self.cfg.get(key, [])))
def _update(*_):
@ -110,6 +116,13 @@ class ConfigApp(tk.Tk):
row = 0
padx_val = 6
pady_val = 4
self._grid_label(row, "输出方式")
save_method = tk.StringVar()
save_method.set(next((c for c in SAVE_METHOD if self.cfg["save_to"].startswith(c)), SAVE_METHOD[0]))
save_method = ttk.Combobox(self, textvariable=save_method, values=SAVE_METHOD, state="readonly", width=20)
save_method.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 编解码器
self._grid_label(row, "编解码器 (h264 / hevc)")
codec_base = tk.StringVar()
@ -231,13 +244,14 @@ class ConfigApp(tk.Tk):
# 按钮
ttk.Button(self, text="保存", command=lambda: self._on_save(codec_base, accel, mode)).grid(row=row, column=0, pady=8, padx=padx_val)
ttk.Button(self, text="保存", command=lambda: self._on_save(save_method,codec_base, accel, mode)).grid(row=row, column=0, pady=8, padx=padx_val)
ttk.Button(self, text="退出", command=self.destroy).grid(row=row, column=1, pady=8)
_switch_mode() # 初始启用/禁用
# ── callbacks -----------------------------------------------------------
def _on_save(self, codec_base_var, accel_var, mode_var):
def _on_save(self, save_method, codec_base_var, accel_var, mode_var):
# 重构codec字符串同时处理显卡品牌映射
save_to = save_method.get()
base = codec_base_var.get()
brand = accel_var.get()
brand_map = {"NVIDIA": "nvenc", "AMD": "amf", "Intel": "qsv", "不使用": ""}

887
VideoCompress/config_ui.py Normal file
View File

@ -0,0 +1,887 @@
import sys
import json
import os
from pathlib import Path
from dataclasses import dataclass
from typing import Dict, List, Optional, Any
from PySide6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QGridLayout,
QWidget, QLabel, QComboBox, QSpinBox, QLineEdit, QPushButton,
QCheckBox, QSlider, QGroupBox, QFileDialog, QMessageBox,
QFrame, QSizePolicy, QToolTip
)
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtGui import QFont, QIcon, QPalette
@dataclass
class VideoConfig:
"""视频压缩配置数据类"""
save_to: str = "multi"
crf: int = 18
codec: str = "h264"
ffmpeg: str = "ffmpeg"
video_ext: List[str] = None
extra: List[str] = None
manual: Optional[List[str]] = None
train: bool = False
bitrate: Optional[str] = None
def __post_init__(self):
if self.video_ext is None:
self.video_ext = [".mp4", ".mkv"]
if self.extra is None:
self.extra = []
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'VideoConfig':
"""从字典创建配置对象"""
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
result = {}
for key, value in self.__dict__.items():
if value is not None:
result[key] = value
return result
class ConfigValidator:
"""配置验证器"""
@staticmethod
def validate_crf(value: int) -> bool:
return 0 <= value <= 51
@staticmethod
def validate_bitrate(value: str) -> bool:
if not value:
return True
return value.endswith(('M', 'k', 'K')) and value[:-1].replace('.', '').isdigit()
@staticmethod
def validate_ffmpeg_path(path: str) -> bool:
if path == "ffmpeg": # 系统PATH中的ffmpeg
return True
return os.path.isfile(path) and path.lower().endswith(('.exe', ''))
class QCollapsibleGroupBox(QGroupBox):
"""可折叠的GroupBox"""
def __init__(self, title: str, parent=None):
super().__init__(title, parent)
self.setCheckable(True)
self.setChecked(False)
self.toggled.connect(self._on_toggled)
# 设置特殊样式
self.setStyleSheet("""
QCollapsibleGroupBox {
font-weight: bold;
border: 2px solid #e9ecef;
border-radius: 8px;
margin-top: 1ex;
padding-top: 10px;
background-color: white;
color: #495057;
}
QCollapsibleGroupBox:hover {
border-color: #ffc107;
background-color: #fffdf5;
}
QCollapsibleGroupBox:checked {
border-color: #28a745;
background-color: #f8fff9;
}
QCollapsibleGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 8px 0 8px;
color: #495057;
background-color: transparent;
}
QCollapsibleGroupBox::indicator {
width: 18px;
height: 18px;
border-radius: 3px;
border: 2px solid #ced4da;
background-color: white;
}
QCollapsibleGroupBox::indicator:hover {
border-color: #ffc107;
background-color: #fffdf5;
}
QCollapsibleGroupBox::indicator:checked {
background-color: #28a745;
border-color: #28a745;
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDNMNC41IDguNUwyIDYiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
}
""")
# 内容区域
self.content_widget = QWidget()
self.content_layout = QVBoxLayout(self.content_widget)
main_layout = QVBoxLayout(self)
main_layout.addWidget(self.content_widget)
self.content_widget.hide()
def _on_toggled(self, checked: bool):
self.content_widget.setVisible(checked)
window:ConfigUI = self.parent().parent()
assert(isinstance(window,ConfigUI))
# 添加展开/收缩的动画效果提示
if checked:
window.setMinimumSize(520,900)
self.setToolTip("点击收起高级设置")
else:
size = window.size()
window.setMinimumSize(520,650)
window.resize(size.width(),size.height()-200)
self.setToolTip("点击展开高级设置")
def addWidget(self, widget):
self.content_layout.addWidget(widget)
def addLayout(self, layout):
self.content_layout.addLayout(layout)
class ConfigUI(QMainWindow):
"""现代化的配置界面"""
# 常量定义
SAVE_METHODS = {
"single": "保存到统一文件夹",
"multi": "每个视频旁建立文件夹"
}
GPU_BRANDS = {
"none": "不使用GPU加速",
"nvidia": "NVIDIA显卡",
"amd": "AMD显卡",
"intel": "Intel核显"
}
CODEC_TYPES = {
"h264": "H.264 (兼容性好)",
"hevc": "H.265 (体积更小)"
}
PRESET_OPTIONS = {
"none": ["", "ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"],
"nvidia": ["", "default", "slow", "medium", "fast", "hp", "hq"],
"amd": ["", "speed", "balanced", "quality"],
"intel": ["", "veryfast", "faster", "fast", "medium", "slow"]
}
def __init__(self):
super().__init__()
self.config = self._load_config()
self._setup_ui()
self._connect_signals()
self._load_values()
def _load_config(self) -> VideoConfig:
"""加载配置文件"""
config_path = self._get_config_path()
try:
if config_path.exists():
with open(config_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return VideoConfig.from_dict(data)
except Exception as e:
QMessageBox.warning(self, "配置加载", f"配置文件加载失败,使用默认设置\n{e}")
return VideoConfig()
def _get_config_path(self) -> Path:
"""获取配置文件路径"""
if os.environ.get("INSTALL", "0") == "1":
return Path(os.getenv("APPDATA", "C:/")) / "VideoCompress" / "config.json"
else:
return Path(sys.path[0]) / "config.json"
def _setup_ui(self):
"""设置用户界面"""
self.setWindowTitle("🎬 视频压缩配置")
self.setMinimumSize(520, 650)
self.resize(520, 700)
self._apply_modern_style()
# 主窗口部件
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
# 添加标题
title_label = QLabel("视频压缩工具配置")
title_label.setStyleSheet("""
QLabel {
font-size: 20px;
font-weight: bold;
color: #343a40;
padding: 10px 0;
border-bottom: 2px solid #e9ecef;
margin-bottom: 10px;
}
""")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
main_layout.addWidget(title_label)
# 基础设置组
basic_group = self._create_basic_group()
main_layout.addWidget(basic_group)
# 质量设置组
quality_group = self._create_quality_group()
main_layout.addWidget(quality_group)
# 硬件加速组
hardware_group = self._create_hardware_group()
main_layout.addWidget(hardware_group)
# 高级设置组(可折叠)
advanced_group = self._create_advanced_group()
main_layout.addWidget(advanced_group)
# 按钮区域
button_layout = self._create_buttons()
main_layout.addLayout(button_layout)
main_layout.addStretch()
def _apply_modern_style(self):
"""应用现代化样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #f8f9fa;
}
QGroupBox {
font-weight: bold;
border: 2px solid #e9ecef;
border-radius: 8px;
margin-top: 1ex;
padding-top: 10px;
background-color: white;
color: #495057;
}
QGroupBox:hover {
border-color: #007bff;
background-color: #f8f9ff;
}
QGroupBox:focus {
border-color: #007bff;
background-color: #f0f4ff;
outline: none;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 8px 0 8px;
color: #495057;
background-color: transparent;
}
QGroupBox::indicator {
width: 18px;
height: 18px;
border-radius: 3px;
border: 2px solid #ced4da;
background-color: white;
}
QGroupBox::indicator:hover {
border-color: #007bff;
background-color: #f8f9ff;
}
QGroupBox::indicator:checked {
background-color: #007bff;
border-color: #007bff;
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDNMNC41IDguNUwyIDYiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
}
QComboBox, QLineEdit, QSpinBox {
border: 1px solid #ced4da;
border-radius: 4px;
padding: 8px;
background-color: white;
font-size: 14px;
color: #495057;
}
QComboBox:hover, QLineEdit:hover, QSpinBox:hover {
border-color: #007bff;
}
QComboBox:focus, QLineEdit:focus, QSpinBox:focus {
border-color: #007bff;
background-color: #f8f9ff;
outline: none;
}
QComboBox::drop-down {
border: none;
background-color: transparent;
width: 30px;
}
QComboBox::down-arrow {
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEyIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xIDEuNUw2IDYuNUwxMSAxLjUiIHN0cm9rZT0iIzQ5NTA1NyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==);
width: 12px;
height: 8px;
}
QPushButton {
background-color: #007bff;
color: white;
border: none;
border-radius: 6px;
padding: 10px 20px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #0056b3;
}
QPushButton:pressed {
background-color: #003f7f;
}
QSlider::groove:horizontal {
border: 1px solid #ced4da;
background: #f8f9fa;
height: 8px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #007bff;
border: 2px solid #0056b3;
width: 20px;
height: 20px;
margin: -7px 0;
border-radius: 10px;
}
QSlider::handle:horizontal:hover {
background: #0056b3;
border-color: #003f7f;
}
QSlider::sub-page:horizontal {
background: #007bff;
border-radius: 4px;
}
QCheckBox {
font-size: 14px;
spacing: 8px;
color: #495057;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 3px;
border: 2px solid #ced4da;
background-color: white;
}
QCheckBox::indicator:hover {
border-color: #007bff;
background-color: #f8f9ff;
}
QCheckBox::indicator:checked {
background-color: #007bff;
border-color: #007bff;
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDNMNC41IDguNUwyIDYiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
}
QLabel {
color: #495057;
font-size: 14px;
}
QToolTip {
background-color: #212529;
color: white;
border: none;
border-radius: 4px;
padding: 8px;
font-size: 12px;
}
""")
def _create_basic_group(self) -> QGroupBox:
"""创建基础设置组"""
group = QGroupBox("⚙️ 基础设置")
layout = QGridLayout(group)
layout.setSpacing(12)
layout.setContentsMargins(15, 20, 15, 15)
# 输出方式
output_label = QLabel("输出方式:")
output_label.setToolTip("选择压缩后的视频文件保存方式")
layout.addWidget(output_label, 0, 0)
self.save_method_combo = QComboBox()
self.save_method_combo.addItems(list(self.SAVE_METHODS.values()))
self.save_method_combo.setToolTip("选择输出文件的保存位置")
layout.addWidget(self.save_method_combo, 0, 1)
# 编码器类型
codec_label = QLabel("编码器:")
codec_label.setToolTip("选择视频编码格式")
layout.addWidget(codec_label, 1, 0)
self.codec_combo = QComboBox()
self.codec_combo.addItems(list(self.CODEC_TYPES.values()))
self.codec_combo.setToolTip("H.264兼容性更好H.265文件更小")
layout.addWidget(self.codec_combo, 1, 1)
return group
def _create_quality_group(self) -> QGroupBox:
"""创建质量设置组"""
group = QGroupBox("质量设置")
layout = QVBoxLayout(group)
layout.setContentsMargins(15, 20, 15, 15)
layout.setSpacing(12)
# CRF/码率选择
mode_layout = QHBoxLayout()
self.crf_radio = QCheckBox("使用CRF (推荐)")
self.bitrate_radio = QCheckBox("使用固定码率")
self.crf_radio.setChecked(True)
self.crf_radio.setToolTip("CRF模式可以保持恒定质量推荐使用")
self.bitrate_radio.setToolTip("固定码率模式可以控制文件大小")
mode_layout.addWidget(self.crf_radio)
mode_layout.addWidget(self.bitrate_radio)
layout.addLayout(mode_layout)
# CRF滑块
crf_layout = QHBoxLayout()
quality_label = QLabel("质量:")
quality_label.setToolTip("数值越小质量越高,文件越大")
crf_layout.addWidget(quality_label)
self.crf_slider = QSlider(Qt.Orientation.Horizontal)
self.crf_slider.setRange(0, 51)
self.crf_slider.setValue(18)
self.crf_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
self.crf_slider.setTickInterval(10)
self.crf_slider.setToolTip("拖动调整视频质量")
crf_layout.addWidget(self.crf_slider)
self.crf_label = QLabel("18 (高质量)")
self.crf_label.setMinimumWidth(80)
self.crf_label.setStyleSheet("font-weight: bold; color: #007bff;")
crf_layout.addWidget(self.crf_label)
layout.addLayout(crf_layout)
# 码率输入
bitrate_layout = QHBoxLayout()
bitrate_label = QLabel("码率:")
bitrate_label.setToolTip("指定视频的码率")
bitrate_layout.addWidget(bitrate_label)
self.bitrate_edit = QLineEdit()
self.bitrate_edit.setPlaceholderText("例如: 2M, 500k")
self.bitrate_edit.setEnabled(False)
self.bitrate_edit.setToolTip("输入目标码率如2M表示2Mbps")
bitrate_layout.addWidget(self.bitrate_edit)
layout.addLayout(bitrate_layout)
return group
def _create_hardware_group(self) -> QGroupBox:
"""创建硬件加速组"""
group = QGroupBox("🚀 硬件加速")
layout = QGridLayout(group)
layout.setContentsMargins(15, 20, 15, 15)
layout.setSpacing(12)
# GPU品牌选择
gpu_label = QLabel("GPU品牌:")
gpu_label.setToolTip("选择您的显卡品牌以启用硬件加速")
layout.addWidget(gpu_label, 0, 0)
self.gpu_combo = QComboBox()
self.gpu_combo.addItems(list(self.GPU_BRANDS.values()))
self.gpu_combo.setToolTip("硬件加速可以显著提升编码速度")
layout.addWidget(self.gpu_combo, 0, 1)
# 预设选择
preset_label = QLabel("压缩预设:")
preset_label.setToolTip("选择编码预设以平衡速度和质量")
layout.addWidget(preset_label, 1, 0)
self.preset_combo = QComboBox()
self.preset_combo.setToolTip("fast模式速度快但质量稍低slow模式质量高但速度慢")
layout.addWidget(self.preset_combo, 1, 1)
return group
def _create_advanced_group(self) -> QCollapsibleGroupBox:
"""创建高级设置组"""
group = QCollapsibleGroupBox("高级设置")
group.setToolTip("点击展开高级设置选项")
# FFmpeg路径
ffmpeg_layout = QHBoxLayout()
ffmpeg_label = QLabel("FFmpeg路径:")
ffmpeg_label.setToolTip("指定FFmpeg程序的路径")
ffmpeg_layout.addWidget(ffmpeg_label)
self.ffmpeg_edit = QLineEdit()
self.ffmpeg_edit.setPlaceholderText("ffmpeg")
self.ffmpeg_edit.setToolTip("留空使用系统PATH中的ffmpeg")
ffmpeg_layout.addWidget(self.ffmpeg_edit)
self.ffmpeg_browse_btn = QPushButton("📂 浏览")
self.ffmpeg_browse_btn.setMaximumWidth(80)
self.ffmpeg_browse_btn.setToolTip("浏览选择FFmpeg可执行文件")
ffmpeg_layout.addWidget(self.ffmpeg_browse_btn)
group.addLayout(ffmpeg_layout)
# 支持的视频格式
ext_layout = QHBoxLayout()
ext_label = QLabel("🎬 视频格式:")
ext_label.setToolTip("指定支持的视频文件格式")
ext_layout.addWidget(ext_label)
self.ext_edit = QLineEdit()
self.ext_edit.setPlaceholderText(".mp4,.mkv,.avi")
self.ext_edit.setToolTip("用逗号分隔多个格式")
ext_layout.addWidget(self.ext_edit)
group.addLayout(ext_layout)
# 自定义参数
custom_layout = QVBoxLayout()
custom_label = QLabel("自定义FFmpeg参数:")
custom_label.setToolTip("高级用户可以添加自定义FFmpeg参数")
custom_layout.addWidget(custom_label)
self.custom_edit = QLineEdit()
self.custom_edit.setPlaceholderText("高级用户使用,例如: -threads 4")
self.custom_edit.setToolTip("添加额外的FFmpeg命令行参数")
custom_layout.addWidget(self.custom_edit)
group.addLayout(custom_layout)
# 实验性功能
self.train_checkbox = QCheckBox("启用训练模式 (实验性)")
self.train_checkbox.setToolTip("实验性功能,可能不稳定")
group.addWidget(self.train_checkbox)
return group
def _create_buttons(self) -> QHBoxLayout:
"""创建按钮区域"""
layout = QHBoxLayout()
layout.setSpacing(10)
layout.addStretch()
# 重置按钮
reset_btn = QPushButton("🔄 重置")
reset_btn.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
border-radius: 6px;
padding: 12px 20px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #545b62;
}
QPushButton:pressed {
background-color: #3d4142;
}
""")
reset_btn.setToolTip("重置所有设置为默认值")
reset_btn.clicked.connect(self._reset_config)
layout.addWidget(reset_btn)
# 保存按钮
save_btn = QPushButton("💾 保存配置")
save_btn.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
border-radius: 6px;
padding: 12px 20px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
QPushButton:pressed {
background-color: #1e7e34;
}
""")
save_btn.setToolTip("保存当前配置")
save_btn.clicked.connect(self._save_config)
layout.addWidget(save_btn)
# 退出按钮
exit_btn = QPushButton("❌ 退出")
exit_btn.setStyleSheet("""
QPushButton {
background-color: #dc3545;
color: white;
border: none;
border-radius: 6px;
padding: 12px 20px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #c82333;
}
QPushButton:pressed {
background-color: #bd2130;
}
""")
exit_btn.setToolTip("退出配置程序")
exit_btn.clicked.connect(self.close)
layout.addWidget(exit_btn)
return layout
def _connect_signals(self):
"""连接信号和槽"""
# CRF滑块更新标签
self.crf_slider.valueChanged.connect(self._update_crf_label)
# CRF/码率模式切换
self.crf_radio.toggled.connect(self._toggle_quality_mode)
self.bitrate_radio.toggled.connect(self._toggle_quality_mode)
# GPU品牌变化时更新预设选项
self.gpu_combo.currentTextChanged.connect(self._update_preset_options)
# FFmpeg浏览按钮
self.ffmpeg_browse_btn.clicked.connect(self._browse_ffmpeg)
# 实时验证
self.bitrate_edit.textChanged.connect(self._validate_bitrate)
def _update_crf_label(self, value: int):
"""更新CRF标签显示"""
if value <= 18:
quality = "高质量"
elif value <= 23:
quality = "平衡"
elif value <= 28:
quality = "压缩优先"
else:
quality = "高压缩"
self.crf_label.setText(f"{value} ({quality})")
def _toggle_quality_mode(self):
"""切换质量模式"""
if self.sender() == self.crf_radio:
if self.crf_radio.isChecked():
self.bitrate_radio.setChecked(False)
self.crf_slider.setEnabled(True)
self.bitrate_edit.setEnabled(False)
else: # bitrate_radio
if self.bitrate_radio.isChecked():
self.crf_radio.setChecked(False)
self.crf_slider.setEnabled(False)
self.bitrate_edit.setEnabled(True)
def _update_preset_options(self, gpu_text: str):
"""根据GPU品牌更新预设选项"""
gpu_key = None
for key, value in self.GPU_BRANDS.items():
if value == gpu_text:
gpu_key = key
break
if gpu_key == "none":
preset_key = "none"
elif gpu_key == "nvidia":
preset_key = "nvidia"
elif gpu_key == "amd":
preset_key = "amd"
elif gpu_key == "intel":
preset_key = "intel"
else:
preset_key = "none"
self.preset_combo.clear()
options = self.PRESET_OPTIONS.get(preset_key, [""])
display_options = ["默认"] + [opt if opt else "" for opt in options[1:]]
self.preset_combo.addItems(display_options)
def _browse_ffmpeg(self):
"""浏览FFmpeg文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择FFmpeg可执行文件", "",
"可执行文件 (*.exe);;所有文件 (*)"
)
if file_path:
self.ffmpeg_edit.setText(file_path)
def _validate_bitrate(self, text: str):
"""验证码率输入"""
if text and not ConfigValidator.validate_bitrate(text):
self.bitrate_edit.setStyleSheet("border: 2px solid #dc3545;")
QToolTip.showText(self.bitrate_edit.mapToGlobal(self.bitrate_edit.rect().center()),
"码率格式错误,应为数字+M/k例如2M、500k")
else:
self.bitrate_edit.setStyleSheet("")
def _load_values(self):
"""加载配置值到界面"""
# 基础设置
save_method_text = self.SAVE_METHODS.get(self.config.save_to, list(self.SAVE_METHODS.values())[0])
self.save_method_combo.setCurrentText(save_method_text)
# 编码器
codec_base = "h264"
if self.config.codec.startswith("hevc"):
codec_base = "hevc"
self.codec_combo.setCurrentText(self.CODEC_TYPES[codec_base])
# 质量设置
if hasattr(self.config, 'bitrate') and self.config.bitrate:
self.bitrate_radio.setChecked(True)
self.bitrate_edit.setText(self.config.bitrate)
self.crf_slider.setEnabled(False)
self.bitrate_edit.setEnabled(True)
else:
print(self.config)
self.crf_radio.setChecked(True)
self.crf_slider.setValue(self.config.crf)
self.bitrate_edit.setEnabled(False)
# 硬件加速
gpu_brand = "none"
if "_nvenc" in self.config.codec:
gpu_brand = "nvidia"
elif "_amf" in self.config.codec:
gpu_brand = "amd"
elif "_qsv" in self.config.codec:
gpu_brand = "intel"
self.gpu_combo.setCurrentText(self.GPU_BRANDS[gpu_brand])
self._update_preset_options(self.GPU_BRANDS[gpu_brand])
# 预设
preset_value = ""
if "-preset" in self.config.extra:
idx = self.config.extra.index("-preset")
if idx + 1 < len(self.config.extra):
preset_value = self.config.extra[idx + 1]
if preset_value:
for i in range(self.preset_combo.count()):
if self.preset_combo.itemText(i) == preset_value:
self.preset_combo.setCurrentIndex(i)
break
# 高级设置
self.ffmpeg_edit.setText(self.config.ffmpeg)
self.ext_edit.setText(",".join(self.config.video_ext))
if self.config.manual:
self.custom_edit.setText(" ".join(self.config.manual))
self.train_checkbox.setChecked(self.config.train)
def _save_config(self):
"""保存配置"""
try:
# 验证输入
if self.bitrate_radio.isChecked():
bitrate = self.bitrate_edit.text().strip()
if bitrate and not ConfigValidator.validate_bitrate(bitrate):
QMessageBox.warning(self, "输入错误", "码率格式不正确!")
return
# 构建配置
config = VideoConfig()
# 基础设置
for key, value in self.SAVE_METHODS.items():
if value == self.save_method_combo.currentText():
config.save_to = key
break
# 编码器
codec_base = "h264"
for key, value in self.CODEC_TYPES.items():
if value == self.codec_combo.currentText():
codec_base = key
break
# GPU加速
gpu_suffix = ""
gpu_text = self.gpu_combo.currentText()
if gpu_text == self.GPU_BRANDS["nvidia"]:
gpu_suffix = "_nvenc"
elif gpu_text == self.GPU_BRANDS["amd"]:
gpu_suffix = "_amf"
elif gpu_text == self.GPU_BRANDS["intel"]:
gpu_suffix = "_qsv"
config.codec = codec_base + gpu_suffix
# 质量设置
if self.crf_radio.isChecked():
config.crf = self.crf_slider.value()
config.bitrate = None
else:
config.bitrate = self.bitrate_edit.text().strip()
# 其他设置
config.ffmpeg = self.ffmpeg_edit.text().strip() or "ffmpeg"
ext_text = self.ext_edit.text().strip()
if ext_text:
config.video_ext = [ext.strip() for ext in ext_text.split(",") if ext.strip()]
config.extra = []
# 预设
preset_text = self.preset_combo.currentText()
if preset_text and preset_text != "默认" and preset_text != "":
config.extra.extend(["-preset", preset_text])
# 自定义参数
custom_text = self.custom_edit.text().strip()
if custom_text:
config.manual = custom_text.split()
config.train = self.train_checkbox.isChecked()
# 保存文件
config_path = self._get_config_path()
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config.to_dict(), f, ensure_ascii=False, indent=4)
QMessageBox.information(self, "保存成功", "配置已保存成功!")
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存配置时出错:\n{e}")
def _reset_config(self):
"""重置为默认配置"""
reply = QMessageBox.question(self, "确认重置", "确定要重置为默认配置吗?")
if reply == QMessageBox.StandardButton.Yes:
self.config = VideoConfig()
self._load_values()
def main():
"""主函数"""
if len(sys.argv)>1:
import main
sys.exit(main.main())
app = QApplication()
app.setStyle("Fusion") # 使用现代风格
# 设置应用信息
app.setApplicationName("视频压缩配置")
app.setApplicationVersion("1.3")
app.setOrganizationName("VideoCompress")
window = ConfigUI()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

55
VideoCompress/installer Normal file
View File

@ -0,0 +1,55 @@
; 脚本由 Inno Setup 脚本向导生成。
; 有关创建 Inno Setup 脚本文件的详细信息,请参阅帮助文档!
#define MyAppName "视频批量压缩"
#define MyAppVersion "1.3"
#define MyAppPublisher "flt"
#define MyAppURL "https://www.flt6.top/"
[Setup]
; 注意AppId 的值唯一标识此应用程序。不要在其他应用程序的安装程序中使用相同的 AppId 值。
; (若要生成新的 GUID请在 IDE 中单击 "工具|生成 GUID"。)
AppId={{91E7F53A-CF6E-4352-B62F-6EE52710603D}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\VideoCompress
DefaultGroupName={#MyAppName}
; 取消注释以下行以在非管理员安装模式下运行 (仅为当前用户安装)。
;PrivilegesRequired=lowest
OutputBaseFilename=mysetup
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "chinesesimp"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "foldercontextmenu"; Description: "为文件夹添加右键菜单"; GroupDescription: "上下文菜单:"; Flags: checkedonce
Name: "folderbackgroundmenu"; Description: "为文件夹空白处添加右键菜单"; GroupDescription: "上下文菜单:"; Flags: checkedonce
[Files]
Source: "E:\0学习\高中\录课\河豚\tools\VideoCompress\config_ui.dist"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; 注意:不要在任何共享系统文件上使用 "Flags: ignoreversion"
[Registry]
; 文件夹右键菜单
Root: HKCR; Subkey: "Directory\shell\VideoCompress"; ValueType: string; ValueName: ""; ValueData: "批量压缩视频"; Tasks: foldercontextmenu; Flags: uninsdeletekey
Root: HKCR; Subkey: "Directory\shell\VideoCompress\command"; ValueType: string; ValueName: ""; ValueData: """{app}\config_ui.exe"" ""%1"""; Tasks: foldercontextmenu
; 文件夹背景右键菜单
Root: HKCR; Subkey: "Directory\Background\shell\VideoCompress"; ValueType: string; ValueName: ""; ValueData: "批量压缩视频"; Tasks: folderbackgroundmenu; Flags: uninsdeletekey
Root: HKCR; Subkey: "Directory\Background\shell\VideoCompress\command"; ValueType: string; ValueName: ""; ValueData: """{app}\config_ui.exe"" ""%V"""; Tasks: folderbackgroundmenu
[Icons]
Name: "{group}\配置"; Description: "打开视频压缩配置"; Filename: "{app}\config_ui.exe"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
[UninstallDelete]
Type: files; Name: "{app}\*"
Type: dirifempty; Name: "{app}"
Type: filesandordirs; Name: "{userappdata}\VideoCompress"

View File

@ -8,6 +8,7 @@ from time import time
from rich.logging import RichHandler
from rich.progress import Progress
from pickle import dumps, loads
from typing import Optional
import atexit
import re
@ -16,6 +17,7 @@ TRAIN = False
ESTI_FILE = Path(sys.path[0])/"esti.out"
CFG_FILE = Path(sys.path[0])/"config.json"
CFG = {
"save_to": "single",
"crf":"18",
"bitrate": None,
"codec": "h264",
@ -214,7 +216,7 @@ def func(sz:int,src=False):
logging.debug("esti time exception", exc_info=e)
return -1 if src else "NaN"
def process_video(video_path: Path, update_func=None):
def process_video(video_path: Path, compress_dir:Optional[Path]=None ,update_func=None):
global esti_data
use=None
sz=video_path.stat().st_size//(1024*1024)
@ -226,9 +228,13 @@ def process_video(video_path: Path, update_func=None):
bgn=time()
# 在视频文件所在目录下创建 compress 子目录(如果不存在)
compress_dir = video_path.parent / "compress"
compress_dir.mkdir(exist_ok=True)
if compress_dir is None:
# 在视频文件所在目录下创建 compress 子目录(如果不存在)
compress_dir = video_path.parent / "compress"
else:
compress_dir /= video_path.parent.relative_to(root)
compress_dir.mkdir(exist_ok=True,parents=True)
# 输出文件路径:与原文件同名,保存在 compress 目录下
output_file = compress_dir / (video_path.stem + video_path.suffix)
@ -317,6 +323,9 @@ def traverse_directory(root_dir: Path):
avg_frame_rate, duration = proc.stdout.strip().split('\n')
tmp = avg_frame_rate.split('/')
avg_frame_rate = float(tmp[0]) / float(tmp[1])
if duration == "N/A":
duration = 1000
logging.error(f"无法获取视频信息: {file}, 时长为N/A默认使用1000s。运行时进度条将出现异常。")
duration = float(duration)
frames[file] = duration * avg_frame_rate
@ -337,8 +346,12 @@ def traverse_directory(root_dir: Path):
def update_progress(x):
prog.update(cur,completed=x)
prog.update(task, completed=completed_start+x)
if CFG["save_to"] == "single":
t = process_video(file, root_dir/"Compress", update_progress)
else:
t = process_video(file, update_progress)
t = process_video(file,update_progress)
prog.stop_task(cur)
prog.remove_task(cur)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
@echo off
echo Packing full.
nuitka --standalone config.py --enable-plugin=upx --onefile --enable-plugin=tk-inter --include-data-files=ffmpeg.exe=ffmpeg.exe --include-data-files=ffprobe.exe=ffprobe.exe
nuitka --standalone config_ui.py --enable-plugin=upx --onefile --enable-plugin=tk-inter --include-data-files=ffmpeg.exe=ffmpeg.exe --include-data-files=ffprobe.exe=ffprobe.exe
rename config.exe full.exe
echo Packing single.
nuitka --standalone main.py --enable-plugin=upx --onefile