mirror of
https://github.com/woodchen-ink/video2gif.git
synced 2025-07-18 13:42:03 +08:00
Compare commits
15 Commits
v1.0.0-e80
...
main
Author | SHA1 | Date | |
---|---|---|---|
5f5e5350a7 | |||
7808d7a555 | |||
78a6ec111f | |||
d407d3f72e | |||
163f71564f | |||
861b4af2d2 | |||
0f8751a2f2 | |||
302dbc805f | |||
6d8fb24807 | |||
|
06ba7c7cea | ||
7647d15c52 | |||
f0844c2284 | |||
91fe9bc3da | |||
0decff023e | |||
93a01d21cf |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal 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"
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -62,7 +62,8 @@ jobs:
|
|||||||
- name: Build with PyInstaller
|
- name: Build with PyInstaller
|
||||||
shell: cmd
|
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;." --clean --noconfirm --log-level=INFO gui.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
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
__pycache__/gui.cpython-311.pyc
|
__pycache__/gui.cpython-311.pyc
|
||||||
|
ffmpeg/ffmpeg.exe
|
||||||
|
@ -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
|
||||||
|
151
gui.py
151
gui.py
@ -6,10 +6,9 @@ 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 路径设置
|
# 设置 FFmpeg 路径
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
# 运行在 PyInstaller 打包后的环境
|
# 运行在 PyInstaller 打包后的环境
|
||||||
ffmpeg_path = os.path.join(sys._MEIPASS, "ffmpeg")
|
ffmpeg_path = os.path.join(sys._MEIPASS, "ffmpeg")
|
||||||
@ -17,6 +16,7 @@ else:
|
|||||||
# 运行在开发环境
|
# 运行在开发环境
|
||||||
ffmpeg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ffmpeg")
|
ffmpeg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ffmpeg")
|
||||||
|
|
||||||
|
# 添加到系统 PATH
|
||||||
if ffmpeg_path not in os.environ["PATH"]:
|
if ffmpeg_path not in os.environ["PATH"]:
|
||||||
os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
|
os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
|
||||||
|
|
||||||
@ -140,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)
|
||||||
@ -220,16 +204,6 @@ 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):
|
|
||||||
"""根据质量设置返回 FFmpeg 参数"""
|
|
||||||
quality = self.quality_var.get()
|
|
||||||
if quality == "high":
|
|
||||||
return ["-quality", "100"]
|
|
||||||
elif quality == "medium":
|
|
||||||
return ["-quality", "75"]
|
|
||||||
else:
|
|
||||||
return ["-quality", "50"]
|
|
||||||
|
|
||||||
def validate_inputs(self):
|
def validate_inputs(self):
|
||||||
"""验证输入参数"""
|
"""验证输入参数"""
|
||||||
try:
|
try:
|
||||||
@ -270,6 +244,7 @@ class VideoToGifConverter:
|
|||||||
# 验证输入
|
# 验证输入
|
||||||
if not self.validate_inputs():
|
if not self.validate_inputs():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 确定输出路径
|
# 确定输出路径
|
||||||
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)
|
||||||
@ -294,65 +269,117 @@ 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路径
|
||||||
|
ffmpeg_exe = os.path.join(
|
||||||
|
ffmpeg_path,
|
||||||
|
"ffmpeg.exe" if platform.system().lower() == "windows" else "ffmpeg",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建基本命令
|
||||||
|
filter_complex = f"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)
|
||||||
|
filter_complex += f",scale={width}:{height}"
|
||||||
|
|
||||||
# 更新状态显示
|
# 更新状态显示
|
||||||
self.status_label.config(
|
self.status_label.config(
|
||||||
text=f"正在生成调色板... {os.path.basename(video_path)}"
|
text=f"正在生成调色板... {os.path.basename(video_path)}"
|
||||||
)
|
)
|
||||||
self.root.update()
|
self.root.update()
|
||||||
# 第一步:生成调色板(添加线程参数)
|
|
||||||
stream = ffmpeg.input(video_path)
|
|
||||||
|
|
||||||
|
# 构建调色板生成命令
|
||||||
|
palette_cmd = [ffmpeg_exe, "-y", "-threads", str(threads)] # 覆盖输出文件
|
||||||
|
|
||||||
|
# 添加时间控制
|
||||||
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, "palettegen")
|
# 创建 startupinfo 对象(用于隐藏 CMD 窗口)
|
||||||
# 添加线程参数
|
startupinfo = None
|
||||||
ffmpeg.run(
|
if platform.system().lower() == "windows":
|
||||||
ffmpeg.output(stream, palette_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(
|
self.status_label.config(
|
||||||
text=f"正在生成GIF... {os.path.basename(video_path)}"
|
text=f"正在生成GIF... {os.path.basename(video_path)}"
|
||||||
)
|
)
|
||||||
self.root.update()
|
self.root.update()
|
||||||
# 第二步:使用调色板生成GIF(添加线程参数)
|
|
||||||
stream = ffmpeg.input(video_path)
|
|
||||||
palette = ffmpeg.input(palette_path)
|
|
||||||
|
|
||||||
|
# 构建GIF生成命令
|
||||||
|
gif_cmd = [ffmpeg_exe, "-y", "-threads", str(threads)]
|
||||||
|
|
||||||
|
# 添加时间控制
|
||||||
if start_time > 0:
|
if start_time > 0:
|
||||||
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
|
gif_cmd.extend(["-ss", str(start_time)])
|
||||||
|
|
||||||
|
gif_cmd.extend(["-i", video_path])
|
||||||
|
|
||||||
if duration:
|
if duration:
|
||||||
stream = ffmpeg.filter(stream, "t", duration=float(duration))
|
gif_cmd.extend(["-t", str(float(duration))])
|
||||||
|
|
||||||
stream = ffmpeg.filter(stream, "fps", fps=fps)
|
gif_cmd.extend(
|
||||||
|
[
|
||||||
if self.size_var.get() == "custom":
|
"-i",
|
||||||
width = int(self.width_var.get())
|
palette_path,
|
||||||
height = self.height_var.get()
|
"-lavfi",
|
||||||
height = -1 if height == "auto" else int(height)
|
f"{filter_complex} [x]; [x][1:v] paletteuse",
|
||||||
stream = ffmpeg.filter(stream, "scale", width=width, height=height)
|
output_path,
|
||||||
|
]
|
||||||
stream = ffmpeg.filter([stream, palette], "paletteuse")
|
|
||||||
# 添加线程参数
|
|
||||||
ffmpeg.run(
|
|
||||||
ffmpeg.output(stream, output_path, threads=threads),
|
|
||||||
overwrite_output=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 打印命令用于调试
|
||||||
|
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):
|
||||||
os.remove(palette_path)
|
os.remove(palette_path)
|
||||||
@ -362,6 +389,8 @@ 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
|
||||||
|
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
# 核心依赖
|
# 核心依赖
|
||||||
ffmpeg-python>=0.2.0
|
|
||||||
Pillow>=9.0.0
|
Pillow>=9.0.0
|
||||||
|
@ -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='icon.ico' # 如果你有图标的话
|
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user