Merge branch 'master' of git.flt6.top:flt/tools

This commit is contained in:
2025-08-01 22:13:14 +08:00
9 changed files with 7340 additions and 13 deletions

12
VideoCompress/config.json Normal file
View File

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

View File

@ -6,9 +6,14 @@ from tkinter import ttk, messagebox, filedialog
import main as main_program import main as main_program
from pathlib import Path 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 = { 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):
"""Commaseparated 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
View File

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

View File

@ -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/"

View File

@ -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()
# 在视频文件所在目录下创建 compress 子目录(如果不存在) if compress_dir is None:
compress_dir = video_path.parent / "compress" # 在视频文件所在目录下创建 compress 子目录(如果不存在)
compress_dir.mkdir(exist_ok=True) compress_dir = video_path.parent / "compress"
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,7 +348,11 @@ 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)
t = process_video(file,update_progress) if CFG["save_to"] == "single":
t = process_video(file, root_dir/"Compress", update_progress)
else:
t = process_video(file, update_progress)
prog.stop_task(cur) prog.stop_task(cur)
prog.remove_task(cur) prog.remove_task(cur)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
@echo off @echo 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
View 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
View File

@ -0,0 +1,3 @@
streamlit>=1.28.0
pubchempy>=1.0.4
rdkit>=2022.9.5