Files
tools/VideoCompress/config.py
2025-05-07 14:31:07 +08:00

287 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import os
import sys
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import main as main_program
CONFIG_NAME = "config.json"
DEFAULT_CONFIG = {
"crf": 18,
"codec": "h264", # could be h264, h264_qsv, h264_nvenc … etc.
"ffmpeg": "ffmpeg",
"video_ext": [".mp4", ".mkv"],
"extra": [],
"manual": None,
"train": False,
}
HW_SUFFIXES = ["amf", "qsv", "nvenc"]
CODECS_BASE = ["h264", "hevc"]
preset_options = {
"不使用":["","ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow",],
"AMD": ["","speed","balanced","quality",],
"Intel": ["","veryfast","faster","fast","medium","slow",],
"NVIDIA": ["","default","slow","medium","fast","hp","hq",]
}
def config_path() -> str:
"""Return path of config file next to the executable / script."""
if getattr(sys, "frozen", False): # PyInstaller executable
base = os.path.dirname(sys.executable)
else:
base = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base, CONFIG_NAME)
def load_config() -> dict:
try:
with open(config_path(), "r", encoding="utf-8") as fp:
data = json.load(fp)
if not isinstance(data, dict):
raise ValueError("config.json root must be an object")
return {**DEFAULT_CONFIG, **data}
except FileNotFoundError:
return DEFAULT_CONFIG.copy()
except Exception as exc:
messagebox.showwarning("FFmpeg Config", f"Invalid config.json using defaults.\n{exc}")
return DEFAULT_CONFIG.copy()
def save_config(cfg: dict):
try:
with open(config_path(), "w", encoding="utf-8") as fp:
json.dump(cfg, fp, ensure_ascii=False, indent=4)
except Exception as exc:
messagebox.showerror("配置中心", f"保存失败:\n{exc}")
else:
messagebox.showinfo("配置中心", "保存成功。")
class ConfigApp(tk.Tk):
def __init__(self):
super().__init__()
# 设置现代化主题和统一内边距
style = ttk.Style(self)
style.theme_use('clam')
self.title("配置中心")
self.resizable(False, False)
self.cfg = load_config()
if "-preset" in self.cfg["extra"]:
idx = self.cfg["extra"].index("-preset")
self.preset = self.cfg["extra"][idx+1]
self.cfg["extra"].pop(idx)
self.cfg["extra"].pop(idx)
else:
self.preset = ""
self._build_ui()
# ── helper --------------------------------------------------------------
def _grid_label(self, row: int, text: str):
tk.Label(self, text=text, anchor="w").grid(row=row, column=0, sticky="w", pady=2, padx=4)
def _str_var(self, key: str):
var = tk.StringVar(value=str(self.cfg.get(key, "")))
var.trace_add("write", lambda *_: self.cfg.__setitem__(key, var.get()))
return var
def _bool_var(self, key: str):
var = tk.BooleanVar(value=bool(self.cfg.get(key)))
var.trace_add("write", lambda *_: self.cfg.__setitem__(key, var.get()))
return var
def _list_var_entry(self, key: str, width: int = 28):
"""Commaseparated list entry bound to config[key]."""
var = tk.StringVar(value=",".join(self.cfg.get(key, [])))
def _update(*_):
self.cfg[key] = [s.strip() for s in var.get().split(",") if s.strip()]
var.trace_add("write", _update)
ent = tk.Entry(self, textvariable=var, width=width)
return ent
# ── UI ------------------------------------------------------------------
def _build_ui(self):
row = 0
padx_val = 6
pady_val = 4
# 编解码器
self._grid_label(row, "编解码器 (h264 / hevc)")
codec_base = tk.StringVar()
codec_base.set(next((c for c in CODECS_BASE if self.cfg["codec"].startswith(c)), "h264"))
codec_menu = ttk.Combobox(self, textvariable=codec_base, values=CODECS_BASE, state="readonly", width=10)
codec_menu.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 显卡品牌(硬件加速)
self._grid_label(row, "GPU加速")
accel = tk.StringVar()
brand_vals = ["不使用", "NVIDIA", "AMD", "Intel"]
def get_brand():
codec = self.cfg["codec"]
if codec.endswith("_nvenc"):
return "NVIDIA"
elif codec.endswith("_amf"):
return "AMD"
elif codec.endswith("_qsv"):
return "Intel"
return "不使用"
accel.set(get_brand())
accel_menu = ttk.Combobox(self, textvariable=accel, values=brand_vals, state="readonly", width=10)
accel_menu.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# CRF或码率选择
mode = tk.StringVar(value="crf")
if "bitrate" in self.cfg:
mode.set("bitrate")
def _switch_mode():
if mode.get() == "crf":
bitrate_ent.configure(state="disabled")
crf_ent.configure(state="normal")
else:
crf_ent.configure(state="disabled")
bitrate_ent.configure(state="normal")
tk.Radiobutton(self, text="使用 CRF", variable=mode, value="crf", command=_switch_mode).grid(row=row, column=0, sticky="w", padx=padx_val, pady=pady_val)
tk.Radiobutton(self, text="使用码率", variable=mode, value="bitrate", command=_switch_mode).grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# CRF输入框并在标签中解释参数含义
self._grid_label(row, "CRF 值(质量常数,数值越低质量越高)")
crf_var = self._str_var("crf")
crf_ent = tk.Entry(self, textvariable=crf_var, width=6)
crf_ent.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 码率输入框 (kbit/s)
self._grid_label(row, "码率 (例如6M, 200k)")
bitrate_var = tk.StringVar(value=str(self.cfg.get("bitrate", "")))
def _update_bitrate(*_):
if mode.get() == "bitrate":
try:
self.cfg["bitrate"] = bitrate_var.get().strip()
except ValueError:
self.cfg.pop("bitrate", None)
bitrate_var.trace_add("write", _update_bitrate)
bitrate_ent = tk.Entry(self, textvariable=bitrate_var, width=8)
bitrate_ent.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 预设选项
self._grid_label(row, "预设 (决定压缩的速度和质量)")
preset_var = tk.StringVar(value=self.preset)
def _update_preset_option(*_):
preset_menu['values'] = preset_options[accel.get()]
preset_menu.set("")
def _update_preset(*_):
print(preset_var.get())
if self.cfg["extra"].count("-preset")>0:
idx = self.cfg["extra"].index("-preset")
self.cfg["extra"].pop(idx)
self.cfg["extra"].pop(idx)
if preset_var.get():
self.cfg["extra"].extend(["-preset", preset_var.get()])
preset_var.trace_add("write", _update_preset)
accel.trace_add("write",_update_preset_option)
preset_menu = ttk.Combobox(self, textvariable=preset_var, values=preset_options[get_brand()], state="readonly", width=10)
preset_menu.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# ffmpeg路径
self._grid_label(row, "ffmpeg 可执行文件")
ffmpeg_var = self._str_var("ffmpeg")
ffmpeg_ent = tk.Entry(self, textvariable=ffmpeg_var, width=28)
ffmpeg_ent.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
ttk.Button(self, text="浏览", command=lambda: self._pick_ffmpeg(ffmpeg_var)).grid(row=row, column=2, padx=2, pady=pady_val)
row += 1
# 视频扩展名列表
self._grid_label(row, "视频扩展名 (.x,.y)")
self._list_var_entry("video_ext").grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 额外参数列表
self._grid_label(row, "额外参数列表")
extra_entry = self._list_var_entry("extra")
extra_entry.grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 手动参数列表
self._grid_label(row, "手动参数列表")
manual_var = tk.StringVar(value="" if self.cfg.get("manual") is None else " ".join(self.cfg["manual"]))
def _update_manual(*_):
txt = manual_var.get().strip()
self.cfg["manual"] = None if not txt else txt.split()
manual_var.trace_add("write", _update_manual)
tk.Entry(self, textvariable=manual_var, width=28).grid(row=row, column=1, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 训练模式复选框
train_var = self._bool_var("train")
tk.Checkbutton(self, text="启用训练(实验性)", variable=train_var).grid(row=row, column=0, columnspan=2, sticky="w", padx=padx_val, pady=pady_val)
row += 1
# 按钮
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=self.destroy).grid(row=row, column=1, pady=8)
_switch_mode() # 初始启用/禁用
# ── callbacks -----------------------------------------------------------
def _on_save(self, codec_base_var, accel_var, mode_var):
# 重构codec字符串同时处理显卡品牌映射
base = codec_base_var.get()
brand = accel_var.get()
brand_map = {"NVIDIA": "nvenc", "AMD": "amf", "Intel": "qsv", "不使用": ""}
if brand != "不使用":
self.cfg["codec"] = f"{base}_{brand_map[brand]}"
else:
self.cfg["codec"] = base
# 处理码率和crf的配置
if mode_var.get() == "crf":
self.cfg.pop("bitrate", None)
else:
br = self.cfg.get("bitrate", "")
if not (br.endswith("M") or br.endswith("k")):
messagebox.showwarning("警告", "码率参数可能配置错误。例如3M, 500k")
tmp = self.cfg["extra"]
idx = 0
if len(tmp) == 0:idx = -1
else:
while True:
if tmp[idx] == "-preset":break
elif idx+2==len(tmp):
idx = -1
break
else: idx+=1
if idx!=-1:
preset = self.cfg["extra"][idx+1]
if preset not in preset_options[brand]:
messagebox.showwarning("警告", "预设(preset)参数可能配置错误!")
save_config(self.cfg)
def _pick_ffmpeg(self, var):
path = filedialog.askopenfilename(title="Select ffmpeg executable")
if path:
var.set(path)
def main():
if len(sys.argv) > 1:
main_program.main()
else:
app = ConfigApp()
app.mainloop()
if __name__ == "__main__":
main()