From 0acfaa1429d9e6596dab962c6ba120cf47901bfb Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Fri, 25 Jul 2025 14:08:06 +0800 Subject: [PATCH 1/2] VideoCompress --- VideoCompress/config.json | 12 + VideoCompress/config.py | 22 +- VideoCompress/config_ui.py | 887 ++++ VideoCompress/installer | 55 + VideoCompress/main.py | 23 +- VideoCompress/nuitka-crash-report.xml | 5766 +++++++++++++++++++++++++ VideoCompress/pack.bat | 2 +- 7 files changed, 6757 insertions(+), 10 deletions(-) create mode 100644 VideoCompress/config.json create mode 100644 VideoCompress/config_ui.py create mode 100644 VideoCompress/installer create mode 100644 VideoCompress/nuitka-crash-report.xml diff --git a/VideoCompress/config.json b/VideoCompress/config.json new file mode 100644 index 0000000..f3b0f8b --- /dev/null +++ b/VideoCompress/config.json @@ -0,0 +1,12 @@ +{ + "save_to": "single", + "crf": 18, + "codec": "h264", + "ffmpeg": "ffmpeg", + "video_ext": [ + ".mp4", + ".mkv" + ], + "extra": [], + "train": false +} \ No newline at end of file diff --git a/VideoCompress/config.py b/VideoCompress/config.py index 04e05a5..cfd7985 100644 --- a/VideoCompress/config.py +++ b/VideoCompress/config.py @@ -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): - """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, []))) 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", "不使用": ""} diff --git a/VideoCompress/config_ui.py b/VideoCompress/config_ui.py new file mode 100644 index 0000000..a70fbc5 --- /dev/null +++ b/VideoCompress/config_ui.py @@ -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() \ No newline at end of file diff --git a/VideoCompress/installer b/VideoCompress/installer new file mode 100644 index 0000000..dc82fad --- /dev/null +++ b/VideoCompress/installer @@ -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" \ No newline at end of file diff --git a/VideoCompress/main.py b/VideoCompress/main.py index 9b9e596..a393bd7 100644 --- a/VideoCompress/main.py +++ b/VideoCompress/main.py @@ -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) diff --git a/VideoCompress/nuitka-crash-report.xml b/VideoCompress/nuitka-crash-report.xml new file mode 100644 index 0000000..f9e0533 --- /dev/null +++ b/VideoCompress/nuitka-crash-report.xml @@ -0,0 +1,5766 @@ + + + + + link @C:\Users\dell\AppData\Local\Temp\tmpgheluqjl.lnk + +LINK : fatal error LNK1104: \xce޷\xa8\xb4\xf2\xbf\xaa\xceļ\xfe\xa1\xb0E:\0学习\高中\录课\河豚\tools\VIDEOC~1\CONFIG~2.DIS\_nuitka_temp.exe\xa1\xb1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VideoCompress/pack.bat b/VideoCompress/pack.bat index 0455c5d..3d51b73 100644 --- a/VideoCompress/pack.bat +++ b/VideoCompress/pack.bat @@ -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 From da42c9662ea9d3f6eb8403e3293a1838f0e830bd Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Fri, 1 Aug 2025 21:24:58 +0800 Subject: [PATCH 2/2] mw tool --- mw_tool/main.py | 632 +++++++++++++++++++++++++++++++++++++++ mw_tool/requirements.txt | 3 + 2 files changed, 635 insertions(+) create mode 100644 mw_tool/main.py create mode 100644 mw_tool/requirements.txt diff --git a/mw_tool/main.py b/mw_tool/main.py new file mode 100644 index 0000000..ba76e79 --- /dev/null +++ b/mw_tool/main.py @@ -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("👆 请在左侧输入要查询的化学物质") diff --git a/mw_tool/requirements.txt b/mw_tool/requirements.txt new file mode 100644 index 0000000..69b682a --- /dev/null +++ b/mw_tool/requirements.txt @@ -0,0 +1,3 @@ +streamlit>=1.28.0 +pubchempy>=1.0.4 +rdkit>=2022.9.5