Merge branch 'master' of git.flt6.top:flt/tools
This commit is contained in:
12
VideoCompress/config.json
Normal file
12
VideoCompress/config.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"save_to": "single",
|
||||||
|
"crf": 18,
|
||||||
|
"codec": "h264",
|
||||||
|
"ffmpeg": "ffmpeg",
|
||||||
|
"video_ext": [
|
||||||
|
".mp4",
|
||||||
|
".mkv"
|
||||||
|
],
|
||||||
|
"extra": [],
|
||||||
|
"train": false
|
||||||
|
}
|
@ -6,9 +6,14 @@ from tkinter import ttk, messagebox, filedialog
|
|||||||
import main as main_program
|
import main as main_program
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
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"
|
CONFIG_NAME = Path(sys.path[0])/"config.json"
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
|
"save_to": "multi",
|
||||||
"crf": 18,
|
"crf": 18,
|
||||||
"codec": "h264", # could be h264, h264_qsv, h264_nvenc … etc.
|
"codec": "h264", # could be h264, h264_qsv, h264_nvenc … etc.
|
||||||
"ffmpeg": "ffmpeg",
|
"ffmpeg": "ffmpeg",
|
||||||
@ -20,6 +25,7 @@ DEFAULT_CONFIG = {
|
|||||||
|
|
||||||
HW_SUFFIXES = ["amf", "qsv", "nvenc"]
|
HW_SUFFIXES = ["amf", "qsv", "nvenc"]
|
||||||
CODECS_BASE = ["h264", "hevc"]
|
CODECS_BASE = ["h264", "hevc"]
|
||||||
|
SAVE_METHOD = ["保存到单一Compress", "每个文件夹下建立Compress"]
|
||||||
preset_options = {
|
preset_options = {
|
||||||
"不使用":["","ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow",],
|
"不使用":["","ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow",],
|
||||||
"AMD": ["","speed","balanced","quality",],
|
"AMD": ["","speed","balanced","quality",],
|
||||||
@ -95,7 +101,7 @@ class ConfigApp(tk.Tk):
|
|||||||
return var
|
return var
|
||||||
|
|
||||||
def _list_var_entry(self, key: str, width: int = 28):
|
def _list_var_entry(self, key: str, width: int = 28):
|
||||||
"""Comma‑separated list entry bound to config[key]."""
|
"""Comma-separated list entry bound to config[key]."""
|
||||||
var = tk.StringVar(value=",".join(self.cfg.get(key, [])))
|
var = tk.StringVar(value=",".join(self.cfg.get(key, [])))
|
||||||
|
|
||||||
def _update(*_):
|
def _update(*_):
|
||||||
@ -110,6 +116,13 @@ class ConfigApp(tk.Tk):
|
|||||||
row = 0
|
row = 0
|
||||||
padx_val = 6
|
padx_val = 6
|
||||||
pady_val = 4
|
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)")
|
self._grid_label(row, "编解码器 (h264 / hevc)")
|
||||||
codec_base = tk.StringVar()
|
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)
|
ttk.Button(self, text="退出", command=self.destroy).grid(row=row, column=1, pady=8)
|
||||||
_switch_mode() # 初始启用/禁用
|
_switch_mode() # 初始启用/禁用
|
||||||
|
|
||||||
# ── callbacks -----------------------------------------------------------
|
# ── 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字符串,同时处理显卡品牌映射
|
# 重构codec字符串,同时处理显卡品牌映射
|
||||||
|
save_to = save_method.get()
|
||||||
base = codec_base_var.get()
|
base = codec_base_var.get()
|
||||||
brand = accel_var.get()
|
brand = accel_var.get()
|
||||||
brand_map = {"NVIDIA": "nvenc", "AMD": "amf", "Intel": "qsv", "不使用": ""}
|
brand_map = {"NVIDIA": "nvenc", "AMD": "amf", "Intel": "qsv", "不使用": ""}
|
||||||
|
887
VideoCompress/config_ui.py
Normal file
887
VideoCompress/config_ui.py
Normal 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()
|
@ -1,8 +1,8 @@
|
|||||||
; 脚本由 Inno Setup 脚本向导生成。
|
; 脚本由 Inno Setup 脚本向导生成。
|
||||||
; 有关创建 Inno Setup 脚本文件的详细信息,请参阅帮助文档!
|
; 有关创建 Inno Setup 脚本文件的详细信息,请参阅帮助文档!
|
||||||
|
|
||||||
#define MyAppName "PDF解密"
|
#define MyAppName "视频批量压缩"
|
||||||
#define MyAppVersion "1.1"
|
#define MyAppVersion "1.3"
|
||||||
#define MyAppPublisher "flt"
|
#define MyAppPublisher "flt"
|
||||||
#define MyAppURL "https://www.flt6.top/"
|
#define MyAppURL "https://www.flt6.top/"
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from time import time
|
|||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
from pickle import dumps, loads
|
from pickle import dumps, loads
|
||||||
|
from typing import Optional
|
||||||
import atexit
|
import atexit
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ TRAIN = False
|
|||||||
ESTI_FILE = Path(sys.path[0])/"esti.out"
|
ESTI_FILE = Path(sys.path[0])/"esti.out"
|
||||||
CFG_FILE = Path(sys.path[0])/"config.json"
|
CFG_FILE = Path(sys.path[0])/"config.json"
|
||||||
CFG = {
|
CFG = {
|
||||||
|
"save_to": "single",
|
||||||
"crf":"18",
|
"crf":"18",
|
||||||
"bitrate": None,
|
"bitrate": None,
|
||||||
"codec": "h264",
|
"codec": "h264",
|
||||||
@ -214,7 +216,7 @@ def func(sz:int,src=False):
|
|||||||
logging.debug("esti time exception", exc_info=e)
|
logging.debug("esti time exception", exc_info=e)
|
||||||
return -1 if src else "NaN"
|
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
|
global esti_data
|
||||||
use=None
|
use=None
|
||||||
sz=video_path.stat().st_size//(1024*1024)
|
sz=video_path.stat().st_size//(1024*1024)
|
||||||
@ -226,9 +228,13 @@ def process_video(video_path: Path, update_func=None):
|
|||||||
|
|
||||||
|
|
||||||
bgn=time()
|
bgn=time()
|
||||||
|
if compress_dir is None:
|
||||||
# 在视频文件所在目录下创建 compress 子目录(如果不存在)
|
# 在视频文件所在目录下创建 compress 子目录(如果不存在)
|
||||||
compress_dir = video_path.parent / "compress"
|
compress_dir = video_path.parent / "compress"
|
||||||
compress_dir.mkdir(exist_ok=True)
|
else:
|
||||||
|
compress_dir /= video_path.parent.relative_to(root)
|
||||||
|
|
||||||
|
compress_dir.mkdir(exist_ok=True,parents=True)
|
||||||
|
|
||||||
# 输出文件路径:与原文件同名,保存在 compress 目录下
|
# 输出文件路径:与原文件同名,保存在 compress 目录下
|
||||||
output_file = compress_dir / (video_path.stem + video_path.suffix)
|
output_file = compress_dir / (video_path.stem + video_path.suffix)
|
||||||
@ -318,6 +324,9 @@ def traverse_directory(root_dir: Path):
|
|||||||
avg_frame_rate, duration = proc.stdout.strip().split('\n')
|
avg_frame_rate, duration = proc.stdout.strip().split('\n')
|
||||||
tmp = avg_frame_rate.split('/')
|
tmp = avg_frame_rate.split('/')
|
||||||
avg_frame_rate = float(tmp[0]) / float(tmp[1])
|
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)
|
duration = float(duration)
|
||||||
frames[file] = duration * avg_frame_rate
|
frames[file] = duration * avg_frame_rate
|
||||||
|
|
||||||
@ -339,8 +348,12 @@ def traverse_directory(root_dir: Path):
|
|||||||
prog.update(cur,completed=x)
|
prog.update(cur,completed=x)
|
||||||
prog.update(task, completed=completed_start+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.stop_task(cur)
|
||||||
prog.remove_task(cur)
|
prog.remove_task(cur)
|
||||||
if t is None:
|
if t is None:
|
||||||
|
5766
VideoCompress/nuitka-crash-report.xml
Normal file
5766
VideoCompress/nuitka-crash-report.xml
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo Packing full.
|
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
|
rename config.exe full.exe
|
||||||
echo Packing single.
|
echo Packing single.
|
||||||
nuitka --standalone main.py --enable-plugin=upx --onefile
|
nuitka --standalone main.py --enable-plugin=upx --onefile
|
||||||
|
632
mw_tool/main.py
Normal file
632
mw_tool/main.py
Normal file
@ -0,0 +1,632 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import pubchempy as pcp
|
||||||
|
from rdkit import Chem
|
||||||
|
from rdkit.Chem import rdMolDescriptors
|
||||||
|
from rdkit.Chem import Draw
|
||||||
|
import requests
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="质量及密度查询",
|
||||||
|
layout="wide"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化 session state
|
||||||
|
if 'compound_data' not in st.session_state:
|
||||||
|
st.session_state.compound_data = None
|
||||||
|
|
||||||
|
def search_compound(query):
|
||||||
|
"""搜索化合物信息"""
|
||||||
|
try:
|
||||||
|
compounds = None
|
||||||
|
try:
|
||||||
|
comp = Chem.MolFromSmiles(query)
|
||||||
|
if comp:
|
||||||
|
compounds = pcp.get_compounds(query, 'smiles', listkey_count=3)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 尝试通过化学式搜索
|
||||||
|
if not (isinstance(compounds, list) and len(compounds) != 0):
|
||||||
|
compounds = pcp.get_compounds(query, 'formula', listkey_count=3)
|
||||||
|
if not (isinstance(compounds, list) and len(compounds) != 0):
|
||||||
|
# 尝试通过名称搜索
|
||||||
|
compounds = pcp.get_compounds(query, 'name', listkey_count=3)
|
||||||
|
|
||||||
|
if isinstance(compounds, list) and len(compounds) > 0:
|
||||||
|
return compounds[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"搜索时发生错误: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def calculate_molecular_weight_from_smiles(smiles):
|
||||||
|
"""从SMILES计算分子量"""
|
||||||
|
try:
|
||||||
|
mol = Chem.MolFromSmiles(smiles)
|
||||||
|
if mol:
|
||||||
|
return rdMolDescriptors.CalcExactMolWt(mol)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"SMILES分子量计算错误: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_molecule_image(inchi=None, smiles=None):
|
||||||
|
"""从SMILES生成分子结构图"""
|
||||||
|
try:
|
||||||
|
if inchi:
|
||||||
|
mol = Chem.MolFromInchi(inchi)
|
||||||
|
elif smiles:
|
||||||
|
mol = Chem.MolFromSmiles(smiles)
|
||||||
|
else:
|
||||||
|
st.error("必须提供InChI或SMILES字符串")
|
||||||
|
return None
|
||||||
|
if mol:
|
||||||
|
# 生成分子图像
|
||||||
|
img = Draw.MolToImage(mol, size=(300, 300))
|
||||||
|
# 将图像转换为字节流
|
||||||
|
img_buffer = BytesIO()
|
||||||
|
img.save("a.png")
|
||||||
|
img.save(img_buffer, format='PNG')
|
||||||
|
img_buffer.seek(0)
|
||||||
|
return img_buffer
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"分子结构图生成错误: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_pubchem_properties(compound):
|
||||||
|
"""从PubChem获取密度、熔点、沸点信息"""
|
||||||
|
try:
|
||||||
|
# 初始化返回数据
|
||||||
|
properties = {
|
||||||
|
'density': None,
|
||||||
|
'melting_point': None,
|
||||||
|
'boiling_point': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 首先检查compound对象是否直接有属性
|
||||||
|
density = getattr(compound, 'density', None)
|
||||||
|
melting_point = getattr(compound, 'melting_point', None)
|
||||||
|
boiling_point = getattr(compound, 'boiling_point', None)
|
||||||
|
|
||||||
|
if density:
|
||||||
|
properties['density'] = density
|
||||||
|
if melting_point:
|
||||||
|
properties['melting_point'] = melting_point
|
||||||
|
if boiling_point:
|
||||||
|
properties['boiling_point'] = boiling_point
|
||||||
|
|
||||||
|
# 如果没有,尝试通过CID获取更多属性
|
||||||
|
cid = compound.cid
|
||||||
|
|
||||||
|
# 尝试获取物理化学性质相关的记录
|
||||||
|
try:
|
||||||
|
url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/compound/{cid}/JSON?response_type=display"
|
||||||
|
data = requests.get(url, timeout=10).json()
|
||||||
|
for section in data["Record"]["Section"]:
|
||||||
|
if section["TOCHeading"] == "Chemical and Physical Properties":
|
||||||
|
for sub in section["Section"]:
|
||||||
|
if sub["TOCHeading"] == "Experimental Properties":
|
||||||
|
for prop in sub["Section"]:
|
||||||
|
prop_heading = prop["TOCHeading"]
|
||||||
|
|
||||||
|
if prop_heading == "Density" and not properties['density']:
|
||||||
|
# 可能有多条不同温度/浓度的记录,逐条返回
|
||||||
|
properties['density'] = [
|
||||||
|
info["Value"]["StringWithMarkup"][0]["String"]
|
||||||
|
for info in prop["Information"]
|
||||||
|
if "Value" in info and "StringWithMarkup" in info["Value"]
|
||||||
|
]
|
||||||
|
|
||||||
|
elif prop_heading == "Melting Point" and not properties['melting_point']:
|
||||||
|
properties['melting_point'] = [
|
||||||
|
info["Value"]["StringWithMarkup"][0]["String"]
|
||||||
|
for info in prop["Information"]
|
||||||
|
if "Value" in info and "StringWithMarkup" in info["Value"]
|
||||||
|
]
|
||||||
|
|
||||||
|
elif prop_heading == "Boiling Point" and not properties['boiling_point']:
|
||||||
|
properties['boiling_point'] = [
|
||||||
|
info["Value"]["StringWithMarkup"][0]["String"]
|
||||||
|
for info in prop["Information"]
|
||||||
|
if "Value" in info and "StringWithMarkup" in info["Value"]
|
||||||
|
]
|
||||||
|
|
||||||
|
return properties
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return properties
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 静默处理异常,返回空的properties字典
|
||||||
|
return {
|
||||||
|
'density': None,
|
||||||
|
'melting_point': None,
|
||||||
|
'boiling_point': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_liquid_at_room_temp(melting_point):
|
||||||
|
"""判断常温下是否为液体(假设常温为25°C)"""
|
||||||
|
if melting_point is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mp = float(melting_point)
|
||||||
|
return mp < 25 # 熔点低于25°C认为是液体
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sync_calculations(compound_data, mmol=None, mass=None, volume=None, changed_field=None):
|
||||||
|
"""同步计算mmol、质量、体积"""
|
||||||
|
if not compound_data:
|
||||||
|
return mmol, mass, volume
|
||||||
|
|
||||||
|
# 确保数值类型转换
|
||||||
|
try:
|
||||||
|
molecular_weight = float(compound_data.get('molecular_weight', 0))
|
||||||
|
density_select = compound_data.get('density_select', None)
|
||||||
|
density = float(density_select) if density_select is not None else None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
st.error("分子量或密度数据格式错误,无法进行计算")
|
||||||
|
return mmol, mass, volume
|
||||||
|
|
||||||
|
if molecular_weight == 0:
|
||||||
|
return mmol, mass, volume
|
||||||
|
|
||||||
|
try:
|
||||||
|
if changed_field == 'mmol' and mmol is not None:
|
||||||
|
# 根据mmol计算质量
|
||||||
|
mass = (mmol / 1000) * molecular_weight # mmol转mol再乘分子量
|
||||||
|
# 如果有密度,计算体积
|
||||||
|
if density and density > 0:
|
||||||
|
volume = mass / density
|
||||||
|
|
||||||
|
elif changed_field == 'mass' and mass is not None:
|
||||||
|
# 根据质量计算mmol
|
||||||
|
mmol = (mass / molecular_weight) * 1000 # 质量除分子量得mol,再转mmol
|
||||||
|
# 如果有密度,计算体积
|
||||||
|
if density and density > 0:
|
||||||
|
volume = mass / density
|
||||||
|
|
||||||
|
elif changed_field == 'volume' and volume is not None and density and density > 0:
|
||||||
|
# 根据体积计算质量
|
||||||
|
mass = volume * density
|
||||||
|
# 根据质量计算mmol
|
||||||
|
mmol = (mass / molecular_weight) * 1000
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"计算错误: {str(e)}")
|
||||||
|
|
||||||
|
return mmol, mass, volume
|
||||||
|
|
||||||
|
# 主界面
|
||||||
|
col1, col2 = st.columns([1, 2])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader("物质查询")
|
||||||
|
query = st.text_input("输入化学式、名称或SMILES:", placeholder="例如: H2O, water, CCO")
|
||||||
|
|
||||||
|
# 添加直接计算分子量选项
|
||||||
|
calc_mw_only = st.checkbox("仅计算分子量(不查询数据库)", help="勾选此项将跳过数据库查询,仅从SMILES计算分子量")
|
||||||
|
|
||||||
|
if st.button("查询" if not calc_mw_only else "计算", type="primary"):
|
||||||
|
if query:
|
||||||
|
with st.spinner("正在处理..."):
|
||||||
|
# 如果选择仅计算分子量,直接从SMILES计算
|
||||||
|
if calc_mw_only:
|
||||||
|
mol_weight = calculate_molecular_weight_from_smiles(query)
|
||||||
|
if mol_weight:
|
||||||
|
compound_data = {
|
||||||
|
'name': "用户输入化合物",
|
||||||
|
'formula': "从SMILES计算",
|
||||||
|
'molecular_weight': mol_weight,
|
||||||
|
'melting_point': None,
|
||||||
|
'density_src': None,
|
||||||
|
'melting_point_src': None,
|
||||||
|
'boiling_point_src': None,
|
||||||
|
'smiles': query,
|
||||||
|
"inchi": None,
|
||||||
|
'found': False
|
||||||
|
}
|
||||||
|
st.session_state.compound_data = compound_data
|
||||||
|
st.success("✅ 分子量计算完成!")
|
||||||
|
else:
|
||||||
|
st.error("❌ 输入的SMILES格式无效")
|
||||||
|
st.session_state.compound_data = None
|
||||||
|
else:
|
||||||
|
# 原有的查询逻辑
|
||||||
|
compound = search_compound(query)
|
||||||
|
|
||||||
|
if compound is not None:
|
||||||
|
# 查询到化合物
|
||||||
|
# 获取PubChem的物理化学性质信息
|
||||||
|
pubchem_properties = get_pubchem_properties(compound)
|
||||||
|
|
||||||
|
compound_data = {
|
||||||
|
'name': compound.iupac_name or compound.synonyms[0] if compound.synonyms else "未知",
|
||||||
|
'formula': compound.molecular_formula,
|
||||||
|
'molecular_weight': compound.molecular_weight,
|
||||||
|
'melting_point': getattr(compound, 'melting_point', None),
|
||||||
|
'density_src': pubchem_properties['density'],
|
||||||
|
'melting_point_src': pubchem_properties['melting_point'],
|
||||||
|
'boiling_point_src': pubchem_properties['boiling_point'],
|
||||||
|
'smiles': compound.canonical_smiles,
|
||||||
|
"inchi": compound.inchi if hasattr(compound, 'inchi') else None,
|
||||||
|
'found': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
st.session_state.compound_data = compound_data
|
||||||
|
|
||||||
|
# 显示查询结果信息
|
||||||
|
if compound_data['density_src'] or compound_data['melting_point_src'] or compound_data['boiling_point_src']:
|
||||||
|
properties_found = []
|
||||||
|
if compound_data['density_src']:
|
||||||
|
properties_found.append("密度")
|
||||||
|
if compound_data['melting_point_src']:
|
||||||
|
properties_found.append("熔点")
|
||||||
|
if compound_data['boiling_point_src']:
|
||||||
|
properties_found.append("沸点")
|
||||||
|
st.success(f"✅ 查询成功!(找到{', '.join(properties_found)}信息)")
|
||||||
|
else:
|
||||||
|
st.success("✅ 查询成功!(未找到物理性质信息)")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 未查询到,检查是否为SMILES
|
||||||
|
if query:
|
||||||
|
mol_weight = calculate_molecular_weight_from_smiles(query)
|
||||||
|
if mol_weight:
|
||||||
|
compound_data = {
|
||||||
|
'name': "未知化合物",
|
||||||
|
'formula': "从SMILES计算",
|
||||||
|
'molecular_weight': mol_weight,
|
||||||
|
'melting_point': None,
|
||||||
|
'density_src': None,
|
||||||
|
'melting_point_src': None,
|
||||||
|
'boiling_point_src': None,
|
||||||
|
'smiles': query,
|
||||||
|
"inchi": None,
|
||||||
|
'found': False
|
||||||
|
}
|
||||||
|
st.session_state.compound_data = compound_data
|
||||||
|
st.warning("⚠️ 未在数据库中找到,但已从SMILES计算分子量")
|
||||||
|
else:
|
||||||
|
st.error("❌ 未找到该化合物,且SMILES格式无效")
|
||||||
|
st.session_state.compound_data = None
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("化合物信息")
|
||||||
|
|
||||||
|
if st.session_state.compound_data:
|
||||||
|
data = st.session_state.compound_data
|
||||||
|
|
||||||
|
# 显示基本信息
|
||||||
|
info_col1, info_col2 = st.columns(2)
|
||||||
|
|
||||||
|
with info_col1:
|
||||||
|
st.metric("物质名称", data['name'])
|
||||||
|
try:
|
||||||
|
molecular_weight_value = float(data['molecular_weight'])
|
||||||
|
st.metric("分子量 (g/mol)", f"{molecular_weight_value:.3f}")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
st.metric("分子量 (g/mol)", "数据格式错误")
|
||||||
|
|
||||||
|
with info_col2:
|
||||||
|
st.metric("化学式", data['formula'])
|
||||||
|
if data['melting_point']:
|
||||||
|
st.metric("熔点 (°C)", data['melting_point'])
|
||||||
|
# 显示分子结构图
|
||||||
|
if data.get('inchi') or data.get('smiles'):
|
||||||
|
st.markdown("分子结构图")
|
||||||
|
mol_img = generate_molecule_image(inchi=data['inchi'], smiles=data['smiles'])
|
||||||
|
if mol_img:
|
||||||
|
st.image(mol_img, caption="分子键线式结构图", width=150)
|
||||||
|
else:
|
||||||
|
st.info("无法生成分子结构图")
|
||||||
|
|
||||||
|
# 添加熔沸点信息的展开区域
|
||||||
|
if data.get('melting_point_src') or data.get('boiling_point_src'):
|
||||||
|
with st.expander("熔沸点信息", expanded=False):
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
if data.get('melting_point_src'):
|
||||||
|
st.markdown("### 熔点数据")
|
||||||
|
melting_data = data['melting_point_src']
|
||||||
|
if isinstance(melting_data, list):
|
||||||
|
for i, mp in enumerate(melting_data, 1):
|
||||||
|
st.write(f"{i}. {mp}")
|
||||||
|
else:
|
||||||
|
st.write(melting_data)
|
||||||
|
with col2:
|
||||||
|
if data.get('boiling_point_src'):
|
||||||
|
st.markdown("### 沸点数据")
|
||||||
|
boiling_data = data['boiling_point_src']
|
||||||
|
if isinstance(boiling_data, list):
|
||||||
|
for i, bp in enumerate(boiling_data, 1):
|
||||||
|
st.write(f"{i}. {bp}")
|
||||||
|
else:
|
||||||
|
st.write(boiling_data)
|
||||||
|
|
||||||
|
# 判断是否为液体
|
||||||
|
melting_data = data['melting_point_src']
|
||||||
|
if isinstance(melting_data, list) and len(melting_data) > 0:
|
||||||
|
import re
|
||||||
|
melting_point = re.search(r'\d*\.\d+', melting_data[0])
|
||||||
|
if melting_point:
|
||||||
|
melting_point = float(melting_point.group())
|
||||||
|
is_liquid = is_liquid_at_room_temp(melting_point)
|
||||||
|
else:
|
||||||
|
is_liquid = False
|
||||||
|
|
||||||
|
# 检测值变化并执行计算
|
||||||
|
def handle_change(field_name, new_value, current_value):
|
||||||
|
try:
|
||||||
|
# 确保都转换为浮点数
|
||||||
|
new_value = float(new_value) if new_value is not None else 0.0
|
||||||
|
current_value = float(current_value) if current_value is not None else 0.0
|
||||||
|
|
||||||
|
if abs(new_value - current_value) > 1e-6: # 避免浮点数比较问题
|
||||||
|
# 同步计算 - 确保数据类型正确
|
||||||
|
try:
|
||||||
|
calc_data = {
|
||||||
|
'molecular_weight': float(data['molecular_weight']),
|
||||||
|
'density_select': float(st.session_state.get('density_select', 0)) if (show_density and st.session_state.get('density_select')) else None
|
||||||
|
}
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
st.error("化合物数据格式错误,无法进行计算")
|
||||||
|
return
|
||||||
|
|
||||||
|
mmol_calc = mass_calc = volume_calc = 0.0
|
||||||
|
|
||||||
|
if field_name == 'mmol':
|
||||||
|
mmol_calc, mass_calc, volume_calc = sync_calculations(
|
||||||
|
calc_data, new_value, None, None, 'mmol'
|
||||||
|
)
|
||||||
|
elif field_name == 'mass':
|
||||||
|
mmol_calc, mass_calc, volume_calc = sync_calculations(
|
||||||
|
calc_data, None, new_value, None, 'mass'
|
||||||
|
)
|
||||||
|
elif field_name == 'volume':
|
||||||
|
mmol_calc, mass_calc, volume_calc = sync_calculations(
|
||||||
|
calc_data, None, None, new_value, 'volume'
|
||||||
|
)
|
||||||
|
elif field_name == 'density':
|
||||||
|
# 密度变化时,如果已有质量,重新计算体积;如果已有体积,重新计算质量
|
||||||
|
current_mass = st.session_state.mass_val
|
||||||
|
current_volume = st.session_state.volume_val
|
||||||
|
|
||||||
|
if current_mass > 0:
|
||||||
|
# 根据质量重新计算体积
|
||||||
|
mmol_calc, mass_calc, volume_calc = sync_calculations(
|
||||||
|
calc_data, None, current_mass, None, 'mass'
|
||||||
|
)
|
||||||
|
elif current_volume > 0:
|
||||||
|
# 根据体积重新计算质量
|
||||||
|
mmol_calc, mass_calc, volume_calc = sync_calculations(
|
||||||
|
calc_data, None, None, current_volume, 'volume'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return # 没有质量或体积数据,无需重新计算
|
||||||
|
|
||||||
|
# 更新session state
|
||||||
|
st.session_state.mmol_val = float(mmol_calc) if mmol_calc is not None else 0.0
|
||||||
|
st.session_state.mass_val = float(mass_calc) if mass_calc is not None else 0.0
|
||||||
|
st.session_state.volume_val = float(volume_calc) if volume_calc is not None else 0.0
|
||||||
|
st.session_state.last_changed = field_name
|
||||||
|
|
||||||
|
# 强制刷新页面以更新输入框的值
|
||||||
|
if field_name != 'density': # 密度变化时不需要rerun,因为已经在密度输入处理中rerun了
|
||||||
|
st.rerun()
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
st.error(f"数值转换错误: {str(e)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 密度显示选项
|
||||||
|
show_density = False
|
||||||
|
if data['density_src']:
|
||||||
|
if is_liquid:
|
||||||
|
show_density = st.checkbox("显示密度信息", value=True)
|
||||||
|
else:
|
||||||
|
show_density = st.checkbox("显示密度信息", value=False)
|
||||||
|
|
||||||
|
if show_density:
|
||||||
|
import re
|
||||||
|
|
||||||
|
# 初始化密度值在session state中
|
||||||
|
if 'density_select' not in st.session_state:
|
||||||
|
st.session_state.density_select = None
|
||||||
|
if 'density_input_value' not in st.session_state:
|
||||||
|
st.session_state.density_input_value = 0.0
|
||||||
|
|
||||||
|
density_data = data['density_src']
|
||||||
|
# print(density_data)
|
||||||
|
|
||||||
|
# 如果密度是列表且长度>1,让用户选择
|
||||||
|
if isinstance(density_data, list) and len(density_data) > 1:
|
||||||
|
st.markdown("**选择密度数据:**")
|
||||||
|
|
||||||
|
# 为每个密度选项提取数值并显示
|
||||||
|
density_options = []
|
||||||
|
density_values = []
|
||||||
|
|
||||||
|
for i, density_str in enumerate(density_data):
|
||||||
|
# 使用正则表达式提取密度数值
|
||||||
|
match = re.search(r'\d*\.\d+', str(density_str))
|
||||||
|
if match:
|
||||||
|
extracted_value = float(match.group())
|
||||||
|
density_options.append(f"{extracted_value:.3f}: {density_str}")
|
||||||
|
density_values.append(extracted_value)
|
||||||
|
else:
|
||||||
|
density_options.append(f"0.000: {density_str} (无法提取数值)")
|
||||||
|
density_values.append(None)
|
||||||
|
|
||||||
|
# 用户选择密度
|
||||||
|
selected_index = st.selectbox(
|
||||||
|
"选择要使用的密度数据:",
|
||||||
|
range(len(density_options)),
|
||||||
|
format_func=lambda x: density_options[x],
|
||||||
|
key="density_selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取选中的密度值
|
||||||
|
if density_values[selected_index] is not None:
|
||||||
|
selected_density_value = density_values[selected_index]
|
||||||
|
st.session_state.density_select = selected_density_value
|
||||||
|
|
||||||
|
# 显示并允许用户修改密度值
|
||||||
|
st.markdown("**密度值 (可修改):**")
|
||||||
|
new_density = st.number_input(
|
||||||
|
"密度 (g/mL)",
|
||||||
|
min_value=0.0,
|
||||||
|
value=float(st.session_state.density_select),
|
||||||
|
step=0.001,
|
||||||
|
format="%.3f",
|
||||||
|
key="density_input",
|
||||||
|
help="选择的密度值,可以手动修改"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检测密度值变化
|
||||||
|
if abs(new_density - st.session_state.density_input_value) > 1e-6:
|
||||||
|
st.session_state.density_select = new_density
|
||||||
|
st.session_state.density_input_value = new_density
|
||||||
|
# 更新compound_data中的密度值用于计算
|
||||||
|
st.session_state.compound_data['density_select'] = new_density
|
||||||
|
handle_change('density', 1, 0)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
else:
|
||||||
|
st.error("所选密度数据无法提取有效数值")
|
||||||
|
|
||||||
|
# 如果密度是单个值或列表长度为1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if isinstance(density_data, list):
|
||||||
|
density_str = str(density_data[0])
|
||||||
|
else:
|
||||||
|
density_str = str(density_data)
|
||||||
|
|
||||||
|
# 提取密度数值
|
||||||
|
match = re.search(r'\d*\.\d+', density_str)
|
||||||
|
if match:
|
||||||
|
density_value = float(match.group())
|
||||||
|
st.session_state.density_select = density_value
|
||||||
|
|
||||||
|
# 显示并允许用户修改密度值
|
||||||
|
st.markdown("**密度值 (可修改):**")
|
||||||
|
new_density = st.number_input(
|
||||||
|
"密度 (g/mL)",
|
||||||
|
min_value=0.0,
|
||||||
|
value=float(st.session_state.density_select),
|
||||||
|
step=0.001,
|
||||||
|
format="%.3f",
|
||||||
|
key="density_input_single",
|
||||||
|
help="提取的密度值,可以手动修改"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检测密度值变化
|
||||||
|
if abs(new_density - st.session_state.density_input_value) > 1e-6:
|
||||||
|
st.session_state.density_select = new_density
|
||||||
|
st.session_state.density_input_value = new_density
|
||||||
|
# 更新compound_data中的密度值用于计算
|
||||||
|
st.session_state.compound_data['density_select'] = new_density
|
||||||
|
handle_change('density', 1, 0)
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.error("无法从密度数据中提取有效数值")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
st.error("密度数据格式错误")
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# 计算器部分
|
||||||
|
st.subheader("用量计算器")
|
||||||
|
|
||||||
|
# 初始化值
|
||||||
|
if 'mmol_val' not in st.session_state:
|
||||||
|
st.session_state.mmol_val = 0.0
|
||||||
|
if 'mass_val' not in st.session_state:
|
||||||
|
st.session_state.mass_val = 0.0
|
||||||
|
if 'volume_val' not in st.session_state:
|
||||||
|
st.session_state.volume_val = 0.0
|
||||||
|
if 'last_changed' not in st.session_state:
|
||||||
|
st.session_state.last_changed = None
|
||||||
|
|
||||||
|
# 创建响应式列布局
|
||||||
|
density_select = st.session_state.get('density_select')
|
||||||
|
if show_density and density_select is not None:
|
||||||
|
calc_col1, calc_col2, calc_col3 = st.columns([1, 1, 1])
|
||||||
|
else:
|
||||||
|
calc_col1, calc_col2 = st.columns([1, 1])
|
||||||
|
calc_col3 = None
|
||||||
|
|
||||||
|
|
||||||
|
with calc_col1:
|
||||||
|
st.markdown("**物质的量**")
|
||||||
|
new_mmol = st.number_input(
|
||||||
|
"用量 (mmol)",
|
||||||
|
min_value=0.0,
|
||||||
|
value=float(st.session_state.mmol_val),
|
||||||
|
step=0.1,
|
||||||
|
format="%.3f",
|
||||||
|
key="mmol_input",
|
||||||
|
help="输入或计算得到的物质的量,单位:毫摩尔"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 检测mmol变化
|
||||||
|
if st.session_state.last_changed != 'mmol':
|
||||||
|
handle_change('mmol', new_mmol, st.session_state.mmol_val)
|
||||||
|
|
||||||
|
with calc_col2:
|
||||||
|
st.markdown("**质量**")
|
||||||
|
new_mass = st.number_input(
|
||||||
|
"质量 (g)",
|
||||||
|
min_value=0.0,
|
||||||
|
value=float(st.session_state.mass_val),
|
||||||
|
step=0.001,
|
||||||
|
format="%.3f",
|
||||||
|
key="mass_input",
|
||||||
|
help="输入或计算得到的质量,单位:克"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检测mass变化
|
||||||
|
if st.session_state.last_changed != 'mass':
|
||||||
|
handle_change('mass', new_mass, st.session_state.mass_val)
|
||||||
|
|
||||||
|
if calc_col3 is not None:
|
||||||
|
with calc_col3:
|
||||||
|
st.markdown("**体积**")
|
||||||
|
new_volume = st.number_input(
|
||||||
|
"体积 (mL)",
|
||||||
|
min_value=0.0,
|
||||||
|
value=float(st.session_state.volume_val),
|
||||||
|
step=0.01,
|
||||||
|
format="%.3f",
|
||||||
|
key="volume_input",
|
||||||
|
help="输入或计算得到的体积,单位:毫升"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检测volume变化
|
||||||
|
if st.session_state.last_changed != 'volume':
|
||||||
|
handle_change('volume', new_volume, st.session_state.volume_val)
|
||||||
|
|
||||||
|
# 重置last_changed状态
|
||||||
|
st.session_state.last_changed = None
|
||||||
|
|
||||||
|
|
||||||
|
# 清零按钮
|
||||||
|
if st.button("清零所有数值", type="secondary"):
|
||||||
|
st.session_state.mmol_val = 0.0
|
||||||
|
st.session_state.mass_val = 0.0
|
||||||
|
st.session_state.volume_val = 0.0
|
||||||
|
st.session_state.last_changed = None
|
||||||
|
st.rerun()
|
||||||
|
st.session_state.mmol_val = 0.0
|
||||||
|
st.session_state.mass_val = 0.0
|
||||||
|
st.session_state.volume_val = 0.0
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
else:
|
||||||
|
st.info("👆 请在左侧输入要查询的化学物质")
|
3
mw_tool/requirements.txt
Normal file
3
mw_tool/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
streamlit>=1.28.0
|
||||||
|
pubchempy>=1.0.4
|
||||||
|
rdkit>=2022.9.5
|
Reference in New Issue
Block a user