first commit

This commit is contained in:
wood chen 2024-11-16 07:51:31 +08:00
commit 252bbca3b7
6 changed files with 875 additions and 0 deletions

131
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,131 @@
name: Build and Release
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
create-release:
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
- name: Generate version number
id: version
run: |
SHA_SHORT=$(git rev-parse --short HEAD)
echo "version=1.0.0-${SHA_SHORT}" >> $GITHUB_OUTPUT
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.version.outputs.version }}
release_name: Release v${{ steps.version.outputs.version }}
draft: false
prerelease: false
build-windows:
needs: create-release
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Download FFmpeg
run: |
mkdir ffmpeg
curl -L https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip -o ffmpeg.zip
7z x ffmpeg.zip -offmpeg
move ffmpeg\ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe ffmpeg\
move ffmpeg\ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe ffmpeg\
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
# 创建启动器脚本
- name: Create launcher script
run: |
echo 'import os, sys; os.environ["PATH"] = os.path.join(os.path.dirname(sys.executable), "ffmpeg") + os.pathsep + os.environ["PATH"]' > launcher.py
type gui.py >> launcher.py
- name: Build with PyInstaller
run: |
pyinstaller --name video2gif --onefile --windowed --add-data "ffmpeg/ffmpeg.exe;ffmpeg" --add-data "ffmpeg/ffprobe.exe;ffmpeg" --add-data "README.md;." launcher.py
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ./dist/video2gif.exe
asset_name: video2gif-windows-${{ needs.create-release.outputs.version }}.exe
asset_content_type: application/octet-stream
build-macos:
needs: create-release
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Download FFmpeg
run: |
mkdir ffmpeg
curl -L https://evermeet.cx/ffmpeg/getrelease/zip -o ffmpeg.zip
unzip ffmpeg.zip -d ffmpeg
curl -L https://evermeet.cx/ffmpeg/getrelease/zip/ffprobe -o ffprobe.zip
unzip ffprobe.zip -d ffmpeg
chmod +x ffmpeg/ffmpeg ffmpeg/ffprobe
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
# 创建启动器脚本
- name: Create launcher script
run: |
echo 'import os, sys; os.environ["PATH"] = os.path.join(os.path.dirname(sys.executable), "ffmpeg") + os.pathsep + os.environ["PATH"]' > launcher.py
cat gui.py >> launcher.py
- name: Build with PyInstaller
run: |
pyinstaller --name video2gif --onefile --windowed --add-data "ffmpeg/ffmpeg:ffmpeg" --add-data "ffmpeg/ffprobe:ffmpeg" --add-data "README.md:." launcher.py
- name: Create DMG
run: |
cd dist
mkdir -p video2gif.app/Contents/MacOS
mkdir -p video2gif.app/Contents/Resources/ffmpeg
mv video2gif video2gif.app/Contents/MacOS/
hdiutil create -volname "Video2Gif" -srcfolder video2gif.app -ov -format UDZO video2gif.dmg
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ./dist/video2gif.dmg
asset_name: video2gif-macos-${{ needs.create-release.outputs.version }}.dmg
asset_content_type: application/x-apple-diskimage

43
README.md Normal file
View File

@ -0,0 +1,43 @@
# 视频转GIF工具
这是一个使用Python编写的视频转GIF工具它提供了一个图形用户界面GUI方便用户将视频文件转换为GIF动画。
## 功能
- 支持拖放视频文件到应用程序窗口进行转换。
- 自动检测FFmpeg是否安装如未安装则提供安装向导。
- 提供多种系统平台的安装和使用说明。
## 安装
### Windows
1. 下载最新的 `video2gif-windows-${version}.exe` 安装包。
2. 双击安装包,按照提示完成安装。
### macOS
1. 下载最新的 `video2gif-macos-${version}.dmg` 安装包。
2. 双击安装包,将 `Video2Gif` 应用程序拖放到 `Applications` 文件夹中。
## 使用方法
1. 启动应用程序。
2. 将视频文件拖放到应用程序窗口中。
3. 点击“转换”按钮开始转换。
4. 转换完成后GIF文件将保存在与视频文件相同的目录下。
## 开发
### 环境搭建
1. 安装Python 3.7及以上版本。
2. 安装依赖库:
```bash
pip install -r requirements.txt
```
3. 运行应用程序:
```bash
python gui.py
```

628
gui.py Normal file
View File

@ -0,0 +1,628 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess
import sys
import platform
import os
import webbrowser
from threading import Thread
import ffmpeg
import traceback # 用于打印详细错误信息
from tkinterdnd2 import * # 导入拖放支持
class FFmpegInstaller:
@staticmethod
def get_ffmpeg_path():
"""获取FFmpeg可执行文件路径"""
try:
# 获取程序运行路径
if getattr(sys, "frozen", False):
# PyInstaller打包后的路径
base_path = sys._MEIPASS
else:
# 开发环境路径
base_path = os.path.dirname(os.path.abspath(__file__))
# FFmpeg路径
ffmpeg_dir = os.path.join(base_path, "ffmpeg")
if platform.system().lower() == "windows":
ffmpeg_path = os.path.join(ffmpeg_dir, "ffmpeg.exe")
else:
ffmpeg_path = os.path.join(ffmpeg_dir, "ffmpeg")
return ffmpeg_path
except Exception as e:
print(f"Error getting FFmpeg path: {e}")
return None
@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:
def __init__(self):
self.check_ffmpeg_installation()
self.root = TkinterDnD.Tk() # 使用支持拖放的Tk
self.root.title("视频转GIF工具")
# 启用文件拖放
self.root.drop_target_register(DND_FILES)
self.root.dnd_bind("<<Drop>>", self.handle_drop)
self.setup_ui()
# 添加拖放处理方法
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):
# 文件选择框
self.file_frame = ttk.LabelFrame(self.root, text="选择文件")
self.file_frame.pack(padx=10, pady=5, fill="x")
self.files_list = tk.Listbox(self.file_frame, height=5)
self.files_list.pack(padx=5, pady=5, fill="x")
btn_frame = ttk.Frame(self.file_frame)
btn_frame.pack(fill="x", padx=5, pady=5)
self.select_btn = ttk.Button(
btn_frame, text="选择视频", command=self.select_files
)
self.select_btn.pack(side="left", padx=5)
self.clear_btn = ttk.Button(
btn_frame,
text="清空列表",
command=lambda: self.files_list.delete(0, tk.END),
)
self.clear_btn.pack(side="left", padx=5)
# 转换设置框
self.settings_frame = ttk.LabelFrame(self.root, text="转换设置")
self.settings_frame.pack(padx=10, pady=5, fill="x")
# 尺寸设置
size_frame = ttk.Frame(self.settings_frame)
size_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(size_frame, text="尺寸设置:").pack(side="left", padx=5)
self.size_var = tk.StringVar(value="original")
ttk.Radiobutton(
size_frame, text="保持原始尺寸", variable=self.size_var, value="original"
).pack(side="left", padx=5)
ttk.Radiobutton(
size_frame, text="自定义尺寸", variable=self.size_var, value="custom"
).pack(side="left", padx=5)
# 自定义尺寸输入框
custom_size_frame = ttk.Frame(self.settings_frame)
custom_size_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(custom_size_frame, text="宽度:").pack(side="left", padx=5)
self.width_var = tk.StringVar(value="480")
self.width_entry = ttk.Entry(
custom_size_frame, textvariable=self.width_var, width=8
)
self.width_entry.pack(side="left", padx=5)
ttk.Label(custom_size_frame, text="高度:").pack(side="left", padx=5)
self.height_var = tk.StringVar(value="auto")
self.height_entry = ttk.Entry(
custom_size_frame, textvariable=self.height_var, width=8
)
self.height_entry.pack(side="left", padx=5)
# FPS设置
fps_frame = ttk.Frame(self.settings_frame)
fps_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(fps_frame, text="FPS:").pack(side="left", padx=5)
self.fps_var = tk.StringVar(value="10")
self.fps_entry = ttk.Entry(fps_frame, textvariable=self.fps_var, width=8)
self.fps_entry.pack(side="left", padx=5)
# 时长控制
duration_frame = ttk.Frame(self.settings_frame)
duration_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(duration_frame, text="时长控制(秒):").pack(side="left", padx=5)
ttk.Label(duration_frame, text="开始时间:").pack(side="left", padx=5)
self.start_time_var = tk.StringVar(value="0")
self.start_time_entry = ttk.Entry(
duration_frame, textvariable=self.start_time_var, width=8
)
self.start_time_entry.pack(side="left", padx=5)
ttk.Label(duration_frame, text="持续时间:").pack(side="left", padx=5)
self.duration_var = tk.StringVar(value="")
self.duration_entry = ttk.Entry(
duration_frame, textvariable=self.duration_var, width=8
)
self.duration_entry.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.pack(fill="x", padx=5, pady=5)
ttk.Label(output_frame, text="输出位置:").pack(side="left", padx=5)
self.output_var = tk.StringVar(value="same")
ttk.Radiobutton(
output_frame, text="与视频相同目录", variable=self.output_var, value="same"
).pack(side="left", padx=5)
ttk.Radiobutton(
output_frame, text="自定义目录", variable=self.output_var, value="custom"
).pack(side="left", padx=5)
self.output_path_var = tk.StringVar()
self.output_path_entry = ttk.Entry(
output_frame, textvariable=self.output_path_var, width=30
)
self.output_path_entry.pack(side="left", padx=5)
self.browse_btn = ttk.Button(
output_frame, text="浏览", command=self.browse_output
)
self.browse_btn.pack(side="left", padx=5)
# 转换按钮
self.convert_btn = ttk.Button(
self.root, text="开始转换", command=self.start_conversion
)
self.convert_btn.pack(pady=10)
# 进度条
self.progress = ttk.Progressbar(self.root, mode="determinate")
self.progress.pack(fill="x", padx=10, pady=5)
# 状态标签
self.status_label = ttk.Label(self.root, text="就绪")
self.status_label.pack(pady=5)
def browse_output(self):
directory = filedialog.askdirectory()
if directory:
self.output_path_var.set(directory)
def select_files(self):
files = filedialog.askopenfilenames(
filetypes=[("Video files", "*.mp4 *.avi *.mov *.mkv")]
)
self.files_list.delete(0, tk.END)
for file in files:
self.files_list.insert(tk.END, file)
def get_quality_settings(self):
"""根据质量设置返回 FFmpeg 参数"""
quality = self.quality_var.get()
if quality == "high":
return ["-quality", "100"]
elif quality == "medium":
return ["-quality", "75"]
else:
return ["-quality", "50"]
# 修改 convert_video_to_gif 方法
def convert_video_to_gif(self, video_path):
try:
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path()
if ffmpeg_path:
# 设置FFmpeg路径
ffmpeg.input.DEFAULT_FFMPEG_PATH = ffmpeg_path
# 确定输出路径
if self.output_var.get() == "same":
output_dir = os.path.dirname(video_path)
else:
output_dir = self.output_path_var.get()
if not output_dir:
raise ValueError("请选择输出目录")
output_path = os.path.join(
output_dir, os.path.splitext(os.path.basename(video_path))[0] + ".gif"
)
# 获取设置值
start_time = float(self.start_time_var.get() or 0)
duration = self.duration_var.get()
fps = int(self.fps_var.get())
# 设置调色板生成
palette_path = os.path.join(output_dir, "palette.png")
# 获取CPU核心数
cpu_count = os.cpu_count() or 1
threads = max(1, min(cpu_count - 1, 8)) # 留一个核心给系统用
# 第一步:生成调色板(添加线程参数)
stream = ffmpeg.input(video_path)
if start_time > 0:
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
if duration:
stream = ffmpeg.filter(stream, "t", duration=float(duration))
stream = ffmpeg.filter(stream, "fps", fps=fps)
if self.size_var.get() == "custom":
width = int(self.width_var.get())
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, "palettegen")
# 添加线程参数
ffmpeg.run(
ffmpeg.output(stream, palette_path, threads=threads),
overwrite_output=True,
)
# 第二步使用调色板生成GIF添加线程参数
stream = ffmpeg.input(video_path)
palette = ffmpeg.input(palette_path)
if start_time > 0:
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
if duration:
stream = ffmpeg.filter(stream, "t", duration=float(duration))
stream = ffmpeg.filter(stream, "fps", fps=fps)
if self.size_var.get() == "custom":
width = int(self.width_var.get())
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")
# 添加线程参数
ffmpeg.run(
ffmpeg.output(stream, output_path, threads=threads),
overwrite_output=True,
)
# 删除临时调色板文件
if os.path.exists(palette_path):
os.remove(palette_path)
return True
except Exception as e:
error_msg = str(e)
print(f"Conversion error: {error_msg}")
messagebox.showerror("错误", f"转换失败:\n{error_msg}")
return False
def start_conversion(self):
def convert():
files = self.files_list.get(0, tk.END)
if not files:
messagebox.showwarning("警告", "请先选择要转换的视频文件")
return
total = len(files)
for i, file in enumerate(files):
current = i + 1
self.status_label.config(
text=f"正在转换: {os.path.basename(file)} ({current}/{total})"
)
success = self.convert_video_to_gif(file)
progress = current / total * 100
self.progress["value"] = progress
self.status_label.config(text=f"转换完成 ({total}/{total})")
self.convert_btn["state"] = "normal"
messagebox.showinfo("完成", f"所有文件转换完成!\n成功转换 {total} 个文件")
self.convert_btn["state"] = "disabled"
self.progress["value"] = 0
Thread(target=convert).start()
def run(self):
self.root.mainloop()
if __name__ == "__main__":
try:
print("程序启动...")
# 先单独测试 FFmpeg
installer = FFmpegInstaller()
ffmpeg_installed = installer.check_ffmpeg()
print(f"FFmpeg 检测结果: {ffmpeg_installed}")
if not ffmpeg_installed:
print("FFmpeg 未安装,将显示安装向导...")
app = VideoToGifConverter()
app.run()
except Exception as e:
print(f"程序运行出错: {str(e)}")
import traceback
traceback.print_exc() # 打印详细错误信息
sys.exit(1)

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
# 核心依赖
ffmpeg-python>=0.2.0 # FFmpeg Python绑定
Pillow>=9.0.0 # 图像处理库(如果需要预览或其他图像处理功能)
# GUI增强可选
ttkthemes>=3.2.0 # 美化GUI主题
ttkwidgets>=0.12.0 # 额外的tkinter部件
# 开发工具(可选)
pytest>=7.0.0 # 测试框架
black>=22.0.0 # 代码格式化
flake8>=4.0.0 # 代码检查
tkinterdnd2>=3.9.0 # 拖放支持

17
setup.cfg Normal file
View File

@ -0,0 +1,17 @@
[metadata]
name = video2gif
version = attr: video2gif.__version__
description = A GUI tool to convert videos to GIF
long_description = file: README.md
long_description_content_type = text/markdown
author = woodchen
author_email = wood@czl.net
url = https://github.com/woodchen-ink/video2gif
[options]
packages = find:
python_requires = >=3.7
install_requires =
ffmpeg-python>=0.2.0
Pillow>=9.0.0
tkinterdnd2>=0.3.0

43
setup.py Normal file
View File

@ -0,0 +1,43 @@
from setuptools import setup, find_packages
setup(
name="video-to-gif-converter",
version="0.1.0",
packages=find_packages(),
install_requires=[
"ffmpeg-python>=0.2.0",
"Pillow>=9.0.0",
],
extras_require={
'gui': [
'ttkthemes>=3.2.0',
'ttkwidgets>=0.12.0',
],
'dev': [
'pytest>=7.0.0',
'black>=22.0.0',
'flake8>=4.0.0',
],
},
python_requires='>=3.7',
# 元数据
author="Your Name",
author_email="your.email@example.com",
description="A simple video to GIF converter with GUI",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
keywords="video, gif, converter, ffmpeg, gui",
url="https://github.com/yourusername/video-to-gif-converter",
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"Topic :: Multimedia :: Video :: Conversion",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
)