Compare commits

...

20 Commits

Author SHA1 Message Date
5f5e5350a7 fix(gui): 简化GIF生成错误处理逻辑,直接解码错误输出 2025-05-24 20:19:27 +08:00
7808d7a555 fix(gui): 隐藏调色板生成和GIF转换过程中的CMD窗口并改进错误处理 2025-05-24 20:02:52 +08:00
78a6ec111f fix(gui): 移除质量设置选项并简化 FFmpeg 调用参数处理 2025-05-24 18:01:59 +08:00
d407d3f72e fix(gui): 更新 FFmpeg 参数以改进 GIF 生成质量和错误处理 2025-05-24 17:36:04 +08:00
163f71564f Merge branch 'main' of https://github.com/woodchen-ink/video2gif 2025-05-24 17:27:53 +08:00
861b4af2d2 Revert "fix(gui): 更新 FFmpeg 调色板生成参数以改善 GIF 创建质量"
This reverts commit 302dbc805f49d1403923369e7f31283309abd973.
2025-05-24 17:26:00 +08:00
0f8751a2f2 fix(gui): 更新 FFmpeg 调色板生成参数并改进错误处理 2025-05-22 10:51:24 +08:00
302dbc805f fix(gui): 更新 FFmpeg 调色板生成参数以改善 GIF 创建质量 2025-05-22 10:39:30 +08:00
6d8fb24807 fix: update FFmpeg quality settings for palette generation and GIF creation 2025-05-22 10:12:32 +08:00
wood chen
06ba7c7cea
Create dependabot.yml 2024-11-16 12:27:56 +08:00
7647d15c52 fix(gui): hide CMD window on Windows for subprocess calls 2024-11-16 10:47:25 +08:00
f0844c2284 refactor: optimize FFmpeg path settings and update build process
Update FFmpeg path settings and optimize the build process in release.yml and gui.py
2024-11-16 10:39:20 +08:00
91fe9bc3da chore(workflow): remove paths-ignore for markdown files in release workflow 2024-11-16 10:18:45 +08:00
0decff023e chore(workflow): update build process with PyInstaller spec file
Update the GitHub Actions workflow to use the PyInstaller spec file for building the application, ensuring proper inclusion of FFmpeg executables and improving the build process.
2024-11-16 10:17:36 +08:00
93a01d21cf chore(workflow): ignore md files in release workflow
update README for FFmpeg built-in and Python version requirement
2024-11-16 10:04:43 +08:00
e809a2e6f7 chore(workflow): simplify PyInstaller build command in release workflow 2024-11-16 09:57:32 +08:00
dac061da63 fix 2024-11-16 09:54:45 +08:00
5d5decf8ff refactor(gui): remove FFmpeg installation check and related UI components 2024-11-16 09:15:58 +08:00
ba7fe58645 chore(workflow): Update build process to use gui.py instead of launcher.py
Update PyInstaller
2024-11-16 09:11:28 +08:00
8788f0b0b9 refactor(gui): streamline FFmpeg path setup and remove installer dialog 2024-11-16 09:05:36 +08:00
9 changed files with 233 additions and 529 deletions

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View File

@ -57,12 +57,13 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
pip install tkinterdnd2>=0.4.2
pip install pyinstaller pip install pyinstaller
- name: Build with PyInstaller - name: Build with PyInstaller
shell: cmd
run: | run: |
pyinstaller --name video2gif --onefile --windowed --add-data "ffmpeg/ffmpeg.exe;ffmpeg" --add-data "ffmpeg/ffprobe.exe;ffmpeg" --add-data "README.md;." launcher.py pyinstaller --name video2gif --onefile --windowed --hidden-import ffmpeg --add-data "ffmpeg/ffmpeg.exe;ffmpeg" --add-data "ffmpeg/ffprobe.exe;ffmpeg" --add-data "README.md;." --clean --noconfirm --log-level=INFO gui.py
- name: Upload Release Asset - name: Upload Release Asset
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

1
.gitignore vendored
View File

@ -1 +1,2 @@
__pycache__/gui.cpython-311.pyc __pycache__/gui.cpython-311.pyc
ffmpeg/ffmpeg.exe

View File

@ -5,8 +5,8 @@
## 功能 ## 功能
- 支持拖放视频文件到应用程序窗口进行转换。 - 支持拖放视频文件到应用程序窗口进行转换。
- 自动检测FFmpeg是否安装如未安装则提供安装向导 - 内置FFmpeg工具进行视频转码, 不需要在系统安装FFmpeg
- 提供多种系统平台的安装和使用说明 - 多平台, 多线程
## 安装 ## 安装
@ -32,7 +32,7 @@
### 环境搭建 ### 环境搭建
1. 安装Python 3.7及以上版本。 1. 安装Python 3.10及以上版本。
2. 安装依赖库: 2. 安装依赖库:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt

667
gui.py
View File

@ -6,388 +6,60 @@ import platform
import os import os
import webbrowser import webbrowser
from threading import Thread from threading import Thread
import ffmpeg import traceback
import traceback # 用于打印详细错误信息
# 根据平台选择拖放实现 # 设置 FFmpeg 路径
PLATFORM = platform.system().lower() if getattr(sys, "frozen", False):
# 运行在 PyInstaller 打包后的环境
ffmpeg_path = os.path.join(sys._MEIPASS, "ffmpeg")
else:
# 运行在开发环境
ffmpeg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ffmpeg")
if PLATFORM == "windows": # 添加到系统 PATH
try: if ffmpeg_path not in os.environ["PATH"]:
from tkinterdnd2 import DND_FILES, TkinterDnD os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
SUPPORT_DND = "tkdnd"
except ImportError:
SUPPORT_DND = None
elif PLATFORM == "darwin": # macOS
SUPPORT_DND = "macos"
else: # Linux or others
SUPPORT_DND = None
class FFmpegInstaller:
@staticmethod
def get_ffmpeg_path():
if getattr(sys, "frozen", False):
# 运行在 PyInstaller 打包后的环境
return os.path.join(sys._MEIPASS, "ffmpeg")
else:
# 运行在开发环境
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "ffmpeg")
# 设置 FFmpeg 路径
ffmpeg_path = get_ffmpeg_path()
if ffmpeg_path not in os.environ["PATH"]:
os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
@staticmethod
def check_ffmpeg():
"""检查是否可以使用FFmpeg"""
try:
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path()
if ffmpeg_path and os.path.exists(ffmpeg_path):
# 使用打包的FFmpeg
if platform.system().lower() == "windows":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
result = subprocess.run(
[ffmpeg_path, "-version"],
capture_output=True,
startupinfo=startupinfo,
)
else:
result = subprocess.run(
[ffmpeg_path, "-version"], capture_output=True
)
return result.returncode == 0
# 尝试系统PATH中的FFmpeg
if platform.system().lower() == "windows":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
result = subprocess.run(
["ffmpeg", "-version"], capture_output=True, startupinfo=startupinfo
)
else:
result = subprocess.run(["ffmpeg", "-version"], capture_output=True)
return result.returncode == 0
except FileNotFoundError:
return False
except Exception as e:
print(f"FFmpeg检测出错: {str(e)}")
return False
@staticmethod
def get_system_info():
"""获取系统信息"""
system = platform.system().lower()
is_64bits = sys.maxsize > 2**32
return system, is_64bits
def show_installation_dialog(self):
"""显示安装向导对话框"""
system, is_64bits = self.get_system_info()
dialog = tk.Toplevel()
dialog.title("FFmpeg 安装向导")
dialog.geometry("500x400")
dialog.transient() # 设置为模态窗口
# 添加说明文本
ttk.Label(
dialog,
text="未检测到 FFmpeg需要先安装 FFmpeg 才能继续使用。",
wraplength=450,
).pack(padx=20, pady=10)
if system == "windows":
self._setup_windows_installer(dialog)
elif system == "darwin": # macOS
self._setup_macos_installer(dialog)
elif system == "linux":
self._setup_linux_installer(dialog)
else:
self._setup_manual_installer(dialog)
dialog.focus_set()
return dialog
def _setup_windows_installer(self, dialog):
"""Windows安装方式"""
ttk.Label(dialog, text="Windows 安装方式:", font=("", 10, "bold")).pack(
padx=20, pady=5
)
# 方式1: Chocolatey
choco_frame = ttk.LabelFrame(dialog, text="方式1使用 Chocolatey推荐")
choco_frame.pack(padx=20, pady=5, fill="x")
ttk.Label(
choco_frame,
text="1. 首先安装 Chocolatey在管理员权限的 PowerShell 中运行:",
wraplength=450,
).pack(padx=5, pady=5)
choco_cmd = """Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"""
cmd_text = ttk.Entry(choco_frame, width=50)
cmd_text.insert(0, choco_cmd)
cmd_text.pack(padx=5, pady=5)
ttk.Button(
choco_frame,
text="复制命令",
command=lambda: self._copy_to_clipboard(choco_cmd),
).pack(pady=5)
ttk.Label(choco_frame, text="2. 然后安装 FFmpeg运行").pack(padx=5, pady=5)
ffmpeg_cmd = "choco install ffmpeg"
cmd_text2 = ttk.Entry(choco_frame, width=50)
cmd_text2.insert(0, ffmpeg_cmd)
cmd_text2.pack(padx=5, pady=5)
ttk.Button(
choco_frame,
text="复制命令",
command=lambda: self._copy_to_clipboard(ffmpeg_cmd),
).pack(pady=5)
# 方式2: 直接下载
direct_frame = ttk.LabelFrame(dialog, text="方式2直接下载")
direct_frame.pack(padx=20, pady=5, fill="x")
ttk.Label(direct_frame, text="从官方网站下载并手动配置环境变量:").pack(
padx=5, pady=5
)
ttk.Button(
direct_frame,
text="打开下载页面",
command=lambda: webbrowser.open("https://ffmpeg.org/download.html"),
).pack(pady=5)
def _setup_macos_installer(self, dialog):
"""macOS安装方式"""
ttk.Label(dialog, text="macOS 安装方式:", font=("", 10, "bold")).pack(
padx=20, pady=5
)
# 方式1: Homebrew
brew_frame = ttk.LabelFrame(dialog, text="方式1使用 Homebrew推荐")
brew_frame.pack(padx=20, pady=5, fill="x")
ttk.Label(
brew_frame, text="1. 首先安装 Homebrew在终端运行", wraplength=450
).pack(padx=5, pady=5)
brew_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
cmd_text = ttk.Entry(brew_frame, width=50)
cmd_text.insert(0, brew_cmd)
cmd_text.pack(padx=5, pady=5)
ttk.Button(
brew_frame,
text="复制命令",
command=lambda: self._copy_to_clipboard(brew_cmd),
).pack(pady=5)
ttk.Label(brew_frame, text="2. 然后安装 FFmpeg运行").pack(padx=5, pady=5)
ffmpeg_cmd = "brew install ffmpeg"
cmd_text2 = ttk.Entry(brew_frame, width=50)
cmd_text2.insert(0, ffmpeg_cmd)
cmd_text2.pack(padx=5, pady=5)
ttk.Button(
brew_frame,
text="复制命令",
command=lambda: self._copy_to_clipboard(ffmpeg_cmd),
).pack(pady=5)
def _setup_linux_installer(self, dialog):
"""Linux安装方式"""
ttk.Label(dialog, text="Linux 安装方式:", font=("", 10, "bold")).pack(
padx=20, pady=5
)
# Ubuntu/Debian
ubuntu_frame = ttk.LabelFrame(dialog, text="Ubuntu/Debian")
ubuntu_frame.pack(padx=20, pady=5, fill="x")
ubuntu_cmd = "sudo apt update && sudo apt install ffmpeg"
cmd_text = ttk.Entry(ubuntu_frame, width=50)
cmd_text.insert(0, ubuntu_cmd)
cmd_text.pack(padx=5, pady=5)
ttk.Button(
ubuntu_frame,
text="复制命令",
command=lambda: self._copy_to_clipboard(ubuntu_cmd),
).pack(pady=5)
# CentOS/RHEL
centos_frame = ttk.LabelFrame(dialog, text="CentOS/RHEL")
centos_frame.pack(padx=20, pady=5, fill="x")
centos_cmd = "sudo yum install epel-release && sudo yum install ffmpeg"
cmd_text2 = ttk.Entry(centos_frame, width=50)
cmd_text2.insert(0, centos_cmd)
cmd_text2.pack(padx=5, pady=5)
ttk.Button(
centos_frame,
text="复制命令",
command=lambda: self._copy_to_clipboard(centos_cmd),
).pack(pady=5)
def _setup_manual_installer(self, dialog):
"""手动安装说明"""
ttk.Label(
dialog, text="请访问 FFmpeg 官方网站下载并安装:", wraplength=450
).pack(padx=20, pady=10)
ttk.Button(
dialog,
text="打开 FFmpeg 官方网站",
command=lambda: webbrowser.open("https://ffmpeg.org/download.html"),
).pack(pady=10)
def _copy_to_clipboard(self, text):
"""复制文本到剪贴板"""
dialog = tk.Toplevel()
dialog.withdraw()
dialog.clipboard_clear()
dialog.clipboard_append(text)
dialog.update()
dialog.destroy()
messagebox.showinfo("提示", "命令已复制到剪贴板!")
class VideoToGifConverter: class VideoToGifConverter:
def __init__(self): def __init__(self):
self.check_ffmpeg_installation()
# 创建主窗口 # 创建主窗口
if SUPPORT_DND == "tkdnd": self.root = tk.Tk()
self.root = TkinterDnD.Tk()
else:
self.root = tk.Tk()
self.root.title("视频转GIF工具") self.root.title("视频转GIF工具")
# 设置拖放支持 # 设置窗口大小和位置
self.setup_dnd() window_width = 800
window_height = 600
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
# 设置窗口样式
self.root.configure(bg="#f0f0f0")
style = ttk.Style()
style.configure("TButton", padding=6)
style.configure("TLabelframe", background="#f0f0f0")
# 设置UI # 设置UI
self.setup_ui() self.setup_ui()
def setup_dnd(self):
"""设置拖放支持"""
if SUPPORT_DND == "tkdnd":
# Windows with tkinterdnd2
self.root.drop_target_register(DND_FILES)
self.root.dnd_bind("<<Drop>>", self.handle_drop)
elif SUPPORT_DND == "macos":
# macOS native drag and drop
self.root.bind("<<MacDropFiles>>", self.handle_macos_drop)
# 启用macOS拖放
self.root.tk.call("tk_getOpenFile", "-setup")
else:
print("Drag and drop not supported on this platform")
def handle_drop(self, event):
"""处理Windows下的文件拖放"""
files = self.root.tk.splitlist(event.data)
self.process_dropped_files(files)
def handle_macos_drop(self, event):
"""处理macOS下的文件拖放"""
# macOS下获取拖放的文件路径
files = self.root.tk.splitlist(self.root.tk.call("::tk::mac::GetDroppedFiles"))
self.process_dropped_files(files)
def process_dropped_files(self, files):
"""处理拖放的文件"""
# 过滤出视频文件
valid_extensions = (".mp4", ".avi", ".mov", ".mkv")
valid_files = [f for f in files if f.lower().endswith(valid_extensions)]
if valid_files:
self.files_list.delete(0, tk.END)
for file in valid_files:
self.files_list.insert(tk.END, file)
else:
messagebox.showwarning(
"警告", "请拖入视频文件支持mp4, avi, mov, mkv格式"
)
# 添加拖放处理方法
def handle_drop(self, event):
"""处理文件拖放"""
files = self.root.tk.splitlist(event.data)
# 过滤出视频文件
valid_extensions = (".mp4", ".avi", ".mov", ".mkv")
valid_files = [f for f in files if f.lower().endswith(valid_extensions)]
if valid_files:
self.files_list.delete(0, tk.END)
for file in valid_files:
self.files_list.insert(tk.END, file)
else:
messagebox.showwarning(
"警告", "请拖入视频文件支持mp4, avi, mov, mkv格式"
)
def check_ffmpeg_installation(self):
"""检查FFmpeg是否已安装"""
installer = FFmpegInstaller()
if not installer.check_ffmpeg():
# 创建一个临时的root窗口来显示消息框
temp_root = tk.Tk()
temp_root.withdraw() # 隐藏临时窗口
response = messagebox.askquestion(
"FFmpeg未安装", "需要安装FFmpeg才能使用本工具。是否查看安装指南"
)
if response == "yes":
dialog = installer.show_installation_dialog()
temp_root.wait_window(dialog) # 等待安装向导窗口关闭
# 再次检查安装
if not installer.check_ffmpeg():
if (
messagebox.askquestion(
"提示", "请安装完FFmpeg后再运行程序。是否退出程序"
)
== "yes"
):
temp_root.destroy()
sys.exit()
else:
temp_root.destroy()
sys.exit()
temp_root.destroy()
def setup_ui(self): def setup_ui(self):
# 文件选择框 # 文件选择框
self.file_frame = ttk.LabelFrame(self.root, text="选择文件") self.file_frame = ttk.LabelFrame(self.root, text="选择文件")
self.file_frame.pack(padx=10, pady=5, fill="x") self.file_frame.pack(padx=10, pady=5, fill="x")
# 添加拖放提示
if SUPPORT_DND:
ttk.Label(
self.file_frame, text="可以直接拖放视频文件到此处", foreground="blue"
).pack(pady=5)
self.files_list = tk.Listbox(self.file_frame, height=5) self.files_list = tk.Listbox(self.file_frame, height=5)
self.files_list.pack(padx=5, pady=5, fill="x") self.files_list.pack(padx=5, pady=5, fill="x")
# 如果是macOS为Listbox添加拖放支持 # 为文件列表添加右键菜单
if SUPPORT_DND == "macos": self.files_list_menu = tk.Menu(self.root, tearoff=0)
self.files_list.bind("<<MacDropFiles>>", self.handle_macos_drop) self.files_list_menu.add_command(label="删除选中", command=self.delete_selected)
self.files_list_menu.add_command(
label="清空列表", command=lambda: self.files_list.delete(0, tk.END)
)
self.files_list.bind("<Button-3>", self.show_context_menu)
btn_frame = ttk.Frame(self.file_frame) btn_frame = ttk.Frame(self.file_frame)
btn_frame.pack(fill="x", padx=5, pady=5) btn_frame.pack(fill="x", padx=5, pady=5)
@ -468,22 +140,6 @@ class VideoToGifConverter:
self.duration_entry.pack(side="left", padx=5) self.duration_entry.pack(side="left", padx=5)
ttk.Label(duration_frame, text="(留空表示全部)").pack(side="left", padx=5) ttk.Label(duration_frame, text="(留空表示全部)").pack(side="left", padx=5)
# 质量设置
quality_frame = ttk.Frame(self.settings_frame)
quality_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(quality_frame, text="质量设置:").pack(side="left", padx=5)
self.quality_var = tk.StringVar(value="medium")
ttk.Radiobutton(
quality_frame, text="高质量", variable=self.quality_var, value="high"
).pack(side="left", padx=5)
ttk.Radiobutton(
quality_frame, text="中等", variable=self.quality_var, value="medium"
).pack(side="left", padx=5)
ttk.Radiobutton(
quality_frame, text="低质量", variable=self.quality_var, value="low"
).pack(side="left", padx=5)
# 输出设置 # 输出设置
output_frame = ttk.Frame(self.settings_frame) output_frame = ttk.Frame(self.settings_frame)
output_frame.pack(fill="x", padx=5, pady=5) output_frame.pack(fill="x", padx=5, pady=5)
@ -521,14 +177,19 @@ class VideoToGifConverter:
# 状态标签 # 状态标签
self.status_label = ttk.Label(self.root, text="就绪") self.status_label = ttk.Label(self.root, text="就绪")
self.status_label.pack(pady=5) self.status_label.pack(pady=5)
# 如果不支持拖放,添加提示
if not SUPPORT_DND: def show_context_menu(self, event):
ttk.Label( """显示右键菜单"""
self.file_frame, try:
text="注意:当前版本不支持拖放功能,请使用'选择视频'按钮", self.files_list_menu.tk_popup(event.x_root, event.y_root)
wraplength=300, finally:
foreground="red", self.files_list_menu.grab_release()
).pack(pady=5)
def delete_selected(self):
"""删除选中的文件"""
selection = self.files_list.curselection()
for index in reversed(selection):
self.files_list.delete(index)
def browse_output(self): def browse_output(self):
directory = filedialog.askdirectory() directory = filedialog.askdirectory()
@ -543,23 +204,47 @@ class VideoToGifConverter:
for file in files: for file in files:
self.files_list.insert(tk.END, file) self.files_list.insert(tk.END, file)
def get_quality_settings(self): def validate_inputs(self):
"""根据质量设置返回 FFmpeg 参数""" """验证输入参数"""
quality = self.quality_var.get() try:
if quality == "high": # 验证FPS
return ["-quality", "100"] fps = int(self.fps_var.get())
elif quality == "medium": if fps <= 0:
return ["-quality", "75"] raise ValueError("FPS必须大于0")
else:
return ["-quality", "50"] # 验证时间设置
start_time = float(self.start_time_var.get() or 0)
if start_time < 0:
raise ValueError("开始时间不能为负数")
if self.duration_var.get():
duration = float(self.duration_var.get())
if duration <= 0:
raise ValueError("持续时间必须大于0")
# 验证自定义尺寸
if self.size_var.get() == "custom":
width = int(self.width_var.get())
if width <= 0:
raise ValueError("宽度必须大于0")
height = self.height_var.get()
if height != "auto":
height = int(height)
if height <= 0:
raise ValueError("高度必须大于0")
return True
except ValueError as e:
messagebox.showerror("输入错误", str(e))
return False
# 修改 convert_video_to_gif 方法
def convert_video_to_gif(self, video_path): def convert_video_to_gif(self, video_path):
try: try:
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path() # 验证输入
if ffmpeg_path: if not self.validate_inputs():
# 设置FFmpeg路径 return False
ffmpeg.input.DEFAULT_FFMPEG_PATH = ffmpeg_path
# 确定输出路径 # 确定输出路径
if self.output_var.get() == "same": if self.output_var.get() == "same":
output_dir = os.path.dirname(video_path) output_dir = os.path.dirname(video_path)
@ -584,54 +269,116 @@ class VideoToGifConverter:
cpu_count = os.cpu_count() or 1 cpu_count = os.cpu_count() or 1
threads = max(1, min(cpu_count - 1, 8)) # 留一个核心给系统用 threads = max(1, min(cpu_count - 1, 8)) # 留一个核心给系统用
# 第一步:生成调色板(添加线程参数) # 获取ffmpeg路径
stream = ffmpeg.input(video_path) ffmpeg_exe = os.path.join(
ffmpeg_path,
"ffmpeg.exe" if platform.system().lower() == "windows" else "ffmpeg",
)
if start_time > 0: # 构建基本命令
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB") filter_complex = f"fps={fps}" # 开始构建滤镜链
if duration:
stream = ffmpeg.filter(stream, "t", duration=float(duration))
stream = ffmpeg.filter(stream, "fps", fps=fps)
# 添加尺寸控制到滤镜链
if self.size_var.get() == "custom": if self.size_var.get() == "custom":
width = int(self.width_var.get()) width = int(self.width_var.get())
height = self.height_var.get() height = self.height_var.get()
height = -1 if height == "auto" else int(height) height = -1 if height == "auto" else int(height)
stream = ffmpeg.filter(stream, "scale", width=width, height=height) filter_complex += f",scale={width}:{height}"
stream = ffmpeg.filter(stream, "palettegen") # 更新状态显示
# 添加线程参数 self.status_label.config(
ffmpeg.run( text=f"正在生成调色板... {os.path.basename(video_path)}"
ffmpeg.output(stream, palette_path, threads=threads),
overwrite_output=True,
) )
self.root.update()
# 第二步使用调色板生成GIF添加线程参数 # 构建调色板生成命令
stream = ffmpeg.input(video_path) palette_cmd = [ffmpeg_exe, "-y", "-threads", str(threads)] # 覆盖输出文件
palette = ffmpeg.input(palette_path)
# 添加时间控制
if start_time > 0: if start_time > 0:
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB") palette_cmd.extend(["-ss", str(start_time)])
palette_cmd.extend(["-i", video_path])
if duration: if duration:
stream = ffmpeg.filter(stream, "t", duration=float(duration)) palette_cmd.extend(["-t", str(float(duration))])
stream = ffmpeg.filter(stream, "fps", fps=fps) # 添加滤镜和输出
palette_cmd.extend(["-vf", f"{filter_complex},palettegen", palette_path])
if self.size_var.get() == "custom": # 打印命令用于调试
width = int(self.width_var.get()) print("调色板生成命令:", " ".join(palette_cmd))
height = self.height_var.get()
height = -1 if height == "auto" else int(height)
stream = ffmpeg.filter(stream, "scale", width=width, height=height)
stream = ffmpeg.filter([stream, palette], "paletteuse") # 创建 startupinfo 对象(用于隐藏 CMD 窗口)
# 添加线程参数 startupinfo = None
ffmpeg.run( if platform.system().lower() == "windows":
ffmpeg.output(stream, output_path, threads=threads), startupinfo = subprocess.STARTUPINFO()
overwrite_output=True, startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# 运行调色板生成命令
process = subprocess.Popen(
palette_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
creationflags=(
subprocess.CREATE_NO_WINDOW
if platform.system().lower() == "windows"
else 0
),
) )
_, stderr = process.communicate()
if process.returncode != 0:
raise RuntimeError(f"调色板生成失败: {stderr.decode()}")
# 更新状态显示
self.status_label.config(
text=f"正在生成GIF... {os.path.basename(video_path)}"
)
self.root.update()
# 构建GIF生成命令
gif_cmd = [ffmpeg_exe, "-y", "-threads", str(threads)]
# 添加时间控制
if start_time > 0:
gif_cmd.extend(["-ss", str(start_time)])
gif_cmd.extend(["-i", video_path])
if duration:
gif_cmd.extend(["-t", str(float(duration))])
gif_cmd.extend(
[
"-i",
palette_path,
"-lavfi",
f"{filter_complex} [x]; [x][1:v] paletteuse",
output_path,
]
)
# 打印命令用于调试
print("GIF生成命令:", " ".join(gif_cmd))
# 运行GIF生成命令
process = subprocess.Popen(
gif_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
creationflags=(
subprocess.CREATE_NO_WINDOW
if platform.system().lower() == "windows"
else 0
),
)
_, stderr = process.communicate()
if process.returncode != 0:
raise RuntimeError(f"GIF生成失败: {stderr.decode()}")
# 删除临时调色板文件 # 删除临时调色板文件
if os.path.exists(palette_path): if os.path.exists(palette_path):
@ -642,30 +389,51 @@ class VideoToGifConverter:
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
print(f"Conversion error: {error_msg}") print(f"Conversion error: {error_msg}")
print(f"Error type: {type(e)}")
traceback.print_exc()
messagebox.showerror("错误", f"转换失败:\n{error_msg}") messagebox.showerror("错误", f"转换失败:\n{error_msg}")
return False return False
def start_conversion(self): def start_conversion(self):
def convert(): def convert():
files = self.files_list.get(0, tk.END) try:
if not files: files = self.files_list.get(0, tk.END)
messagebox.showwarning("警告", "请先选择要转换的视频文件") if not files:
return messagebox.showwarning("警告", "请先选择要转换的视频文件")
return
total = len(files) total = len(files)
success_count = 0
for i, file in enumerate(files): for i, file in enumerate(files):
current = i + 1 current = i + 1
self.status_label.config( self.status_label.config(
text=f"正在转换: {os.path.basename(file)} ({current}/{total})" text=f"正在转换: {os.path.basename(file)} ({current}/{total})"
) )
success = self.convert_video_to_gif(file) if self.convert_video_to_gif(file):
progress = current / total * 100 success_count += 1
self.progress["value"] = progress progress = current / total * 100
self.progress["value"] = progress
self.status_label.config(text=f"转换完成 ({total}/{total})") self.status_label.config(text=f"转换完成 ({success_count}/{total})")
self.convert_btn["state"] = "normal" self.convert_btn["state"] = "normal"
messagebox.showinfo("完成", f"所有文件转换完成!\n成功转换 {total} 个文件")
if success_count == total:
messagebox.showinfo(
"完成", f"所有文件转换完成!\n成功转换 {total} 个文件"
)
else:
messagebox.showwarning(
"完成",
f"转换完成,但有部分失败。\n成功:{success_count}/{total}",
)
except Exception as e:
print(f"Conversion error: {str(e)}")
traceback.print_exc()
messagebox.showerror("错误", f"转换过程出错:\n{str(e)}")
finally:
self.convert_btn["state"] = "normal"
self.convert_btn["state"] = "disabled" self.convert_btn["state"] = "disabled"
self.progress["value"] = 0 self.progress["value"] = 0
@ -678,13 +446,6 @@ class VideoToGifConverter:
if __name__ == "__main__": if __name__ == "__main__":
try: try:
print("程序启动...") print("程序启动...")
# 先单独测试 FFmpeg
installer = FFmpegInstaller()
ffmpeg_installed = installer.check_ffmpeg()
print(f"FFmpeg 检测结果: {ffmpeg_installed}")
if not ffmpeg_installed:
print("FFmpeg 未安装,将显示安装向导...")
app = VideoToGifConverter() app = VideoToGifConverter()
app.run() app.run()

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,52 +0,0 @@
import os
import sys
import traceback
def get_ffmpeg_path():
if getattr(sys, "frozen", False):
# 运行在 PyInstaller 打包后的环境
return os.path.join(sys._MEIPASS, "ffmpeg")
else:
# 运行在开发环境
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "ffmpeg")
def setup_environment():
try:
# 设置 FFmpeg 路径
ffmpeg_path = get_ffmpeg_path()
if ffmpeg_path not in os.environ["PATH"]:
os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
print(f"FFmpeg path: {ffmpeg_path}")
print(f"System PATH: {os.environ['PATH']}")
except Exception as e:
print(f"Error setting up environment: {str(e)}")
traceback.print_exc()
if __name__ == "__main__":
try:
print("Starting Video2Gif Converter...")
print(f"Python version: {sys.version}")
setup_environment()
from gui import VideoToGifConverter
app = VideoToGifConverter()
app.run()
except Exception as e:
print(f"Error: {str(e)}")
traceback.print_exc()
# 写入错误日志
with open("error.log", "w") as f:
f.write(f"Error: {str(e)}\n")
f.write(traceback.format_exc())
# 如果是打包后的程序,等待用户确认
if getattr(sys, "frozen", False):
input("Press Enter to exit...")
sys.exit(1)

View File

@ -1,16 +1,2 @@
# 核心依赖 # 核心依赖
ffmpeg-python>=0.2.0
Pillow>=9.0.0 Pillow>=9.0.0
# Windows平台依赖
tkinterdnd2>=0.4.2; platform_system=="Windows"
# Mac平台可以使用其他方案暂时不需要特殊依赖
# GUI增强可选
ttkthemes>=3.2.0 # 美化GUI主题
ttkwidgets>=0.12.0 # 额外的tkinter部件
# 开发工具(可选)
pytest>=7.0.0 # 测试框架
black>=22.0.0 # 代码格式化
flake8>=4.0.0 # 代码检查

View File

@ -1,7 +1,4 @@
# video2gif.spec # video2gif.spec
import sys
from PyInstaller.utils.hooks import collect_data_files
block_cipher = None block_cipher = None
a = Analysis( a = Analysis(
@ -13,7 +10,7 @@ a = Analysis(
('ffmpeg/ffprobe.exe', 'ffmpeg'), ('ffmpeg/ffprobe.exe', 'ffmpeg'),
('README.md', '.') ('README.md', '.')
], ],
hiddenimports=['tkinter', 'tkinter.ttk', 'tkinterdnd2'], hiddenimports=['ffmpeg', 'ffmpeg-python'],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
@ -45,5 +42,4 @@ exe = EXE(
target_arch=None, target_arch=None,
codesign_identity=None, codesign_identity=None,
entitlements_file=None, entitlements_file=None,
icon='icons/favicon.ico' # 如果你有图标的话
) )