mirror of
https://github.com/woodchen-ink/video2webp.git
synced 2025-07-18 05:51:58 +08:00
first commit
This commit is contained in:
commit
ed4f923c3b
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"
|
160
.github/workflows/release.yml
vendored
Normal file
160
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,160 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write # 添加这个权限声明
|
||||
|
||||
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: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
name: Release v${{ steps.version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
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.11'
|
||||
|
||||
- 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: Build with PyInstaller
|
||||
shell: cmd
|
||||
run: |
|
||||
pyinstaller --name video2webp --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
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ needs.create-release.outputs.version }}
|
||||
files: ./dist/video2webp.exe
|
||||
|
||||
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.11'
|
||||
|
||||
- name: Download FFmpeg
|
||||
run: |
|
||||
mkdir ffmpeg
|
||||
curl -L https://github.com/eugeneware/ffmpeg-static/releases/download/b4.4/darwin-x64 -o ffmpeg/ffmpeg
|
||||
curl -L https://github.com/eugeneware/ffmpeg-static/releases/download/b4.4/darwin-x64 -o ffmpeg/ffprobe
|
||||
chmod +x ffmpeg/ffmpeg ffmpeg/ffprobe
|
||||
|
||||
# 或者使用 Homebrew(备选方案)
|
||||
- name: Install FFmpeg using Homebrew
|
||||
if: failure()
|
||||
run: |
|
||||
brew install ffmpeg
|
||||
mkdir -p ffmpeg
|
||||
cp $(which ffmpeg) ffmpeg/
|
||||
cp $(which ffprobe) 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
|
||||
cat gui.py >> launcher.py
|
||||
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
pyinstaller --name video2webp --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 video2webp.app/Contents/MacOS
|
||||
mkdir -p video2webp.app/Contents/Resources/ffmpeg
|
||||
mv video2webp video2webp.app/Contents/MacOS/
|
||||
cp ../ffmpeg/* video2webp.app/Contents/Resources/ffmpeg/
|
||||
|
||||
# 创建 Info.plist
|
||||
cat > video2webp.app/Contents/Info.plist << EOL
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>video2webp</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.example.video2webp</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Video2Webp</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOL
|
||||
|
||||
# 创建 DMG
|
||||
hdiutil create -volname "Video2Webp" -srcfolder video2webp.app -ov -format UDZO video2webp.dmg
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ needs.create-release.outputs.version }}
|
||||
files: ./dist/video2webp.dmg
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/gui.cpython-311.pyc
|
43
README.md
Normal file
43
README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# 视频转WEBP工具
|
||||
|
||||
这是一个使用Python编写的视频转WEBP工具,它提供了一个图形用户界面(GUI),方便用户将视频文件转换为WEBP动画。
|
||||
|
||||
## 功能
|
||||
|
||||
- 支持拖放视频文件到应用程序窗口进行转换。
|
||||
- 内置FFmpeg工具进行视频转码, 不需要在系统安装FFmpeg。
|
||||
- 多平台, 多线程。
|
||||
|
||||
## 安装
|
||||
|
||||
### Windows
|
||||
|
||||
1. 下载最新的 `video2webp-windows-${version}.exe` 安装包。
|
||||
2. 双击安装包,按照提示完成安装。
|
||||
|
||||
### macOS
|
||||
|
||||
1. 下载最新的 `video2webp-macos-${version}.dmg` 安装包。
|
||||
2. 双击安装包,将 `Video2Webp` 应用程序拖放到 `Applications` 文件夹中。
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 启动应用程序。
|
||||
2. 将视频文件拖放到应用程序窗口中。
|
||||
3. 点击“转换”按钮开始转换。
|
||||
4. 转换完成后,WEBP文件将保存在与视频文件相同的目录下。
|
||||
|
||||
## 开发
|
||||
|
||||
### 环境搭建
|
||||
|
||||
1. 安装Python 3.10及以上版本。
|
||||
2. 安装依赖库:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. 运行应用程序:
|
||||
```bash
|
||||
python gui.py
|
||||
```
|
476
gui.py
Normal file
476
gui.py
Normal file
@ -0,0 +1,476 @@
|
||||
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 traceback
|
||||
|
||||
# 设置 FFmpeg 路径
|
||||
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")
|
||||
|
||||
# 添加到系统 PATH
|
||||
if ffmpeg_path not in os.environ["PATH"]:
|
||||
os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
|
||||
|
||||
|
||||
def get_subprocess_config():
|
||||
"""获取subprocess配置,用于隐藏控制台窗口"""
|
||||
if platform.system().lower() == "windows":
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
return {
|
||||
"startupinfo": startupinfo,
|
||||
"creationflags": subprocess.CREATE_NO_WINDOW,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
class VideoToWebpConverter:
|
||||
def __init__(self):
|
||||
# 创建主窗口
|
||||
self.root = tk.Tk()
|
||||
self.root.title("视频或gif转WEBP工具")
|
||||
|
||||
# 设置窗口大小和位置
|
||||
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
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
# 显示支持的格式
|
||||
supported_formats_label = ttk.Label(
|
||||
self.root, text="支持的格式为\"*.mp4 *.avi *.mov *.mkv *.webm *.gif'"
|
||||
)
|
||||
supported_formats_label.pack(pady=5)
|
||||
# 文件选择框
|
||||
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")
|
||||
|
||||
# 为文件列表添加右键菜单
|
||||
self.files_list_menu = tk.Menu(self.root, tearoff=0)
|
||||
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.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 show_context_menu(self, event):
|
||||
"""显示右键菜单"""
|
||||
try:
|
||||
self.files_list_menu.tk_popup(event.x_root, event.y_root)
|
||||
finally:
|
||||
self.files_list_menu.grab_release()
|
||||
|
||||
def delete_selected(self):
|
||||
"""删除选中的文件"""
|
||||
selection = self.files_list.curselection()
|
||||
for index in reversed(selection):
|
||||
self.files_list.delete(index)
|
||||
|
||||
def browse_output(self):
|
||||
directory = filedialog.askdirectory()
|
||||
if directory:
|
||||
self.output_path_var.set(directory)
|
||||
|
||||
def select_files(self):
|
||||
files = filedialog.askopenfilenames(
|
||||
filetypes=[
|
||||
("All supported formats", "*.mp4 *.avi *.mov *.mkv *.webm *.gif"),
|
||||
("Video files", "*.mp4 *.avi *.mov *.mkv *.webm"),
|
||||
("GIF files", "*.gif"),
|
||||
]
|
||||
)
|
||||
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"]
|
||||
|
||||
def validate_inputs(self):
|
||||
"""验证输入参数"""
|
||||
try:
|
||||
# 验证FPS
|
||||
fps = int(self.fps_var.get())
|
||||
if fps <= 0:
|
||||
raise ValueError("FPS必须大于0")
|
||||
|
||||
# 验证时间设置
|
||||
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
|
||||
|
||||
def convert_to_webp(self, input_path):
|
||||
try:
|
||||
# 验证输入
|
||||
if not self.validate_inputs():
|
||||
return False
|
||||
|
||||
# 确定输出路径
|
||||
if self.output_var.get() == "same":
|
||||
output_dir = os.path.dirname(input_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(input_path))[0] + ".webp"
|
||||
)
|
||||
|
||||
# 获取设置值
|
||||
start_time = float(self.start_time_var.get() or 0)
|
||||
duration = self.duration_var.get()
|
||||
fps = int(self.fps_var.get())
|
||||
|
||||
# 获取CPU核心数
|
||||
cpu_count = os.cpu_count() or 1
|
||||
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",
|
||||
)
|
||||
|
||||
# 检查输入文件类型
|
||||
is_gif = input_path.lower().endswith(".gif")
|
||||
|
||||
# 构建命令
|
||||
cmd = [ffmpeg_exe, "-y", "-threads", str(threads)] # 覆盖输出文件
|
||||
|
||||
# 添加时间控制 (仅对视频有效)
|
||||
if not is_gif and start_time > 0:
|
||||
cmd.extend(["-ss", str(start_time)])
|
||||
|
||||
cmd.extend(["-i", input_path])
|
||||
|
||||
if not is_gif and duration:
|
||||
cmd.extend(["-t", str(float(duration))])
|
||||
|
||||
# 构建滤镜链
|
||||
filters = []
|
||||
|
||||
# FPS控制
|
||||
if (
|
||||
not is_gif or self.fps_var.get() != "10"
|
||||
): # GIF默认保持原FPS,除非用户修改
|
||||
filters.append(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)
|
||||
filters.append(f"scale={width}:{height}")
|
||||
|
||||
# 添加滤镜
|
||||
if filters:
|
||||
cmd.extend(["-vf", ",".join(filters)])
|
||||
|
||||
# 添加质量设置
|
||||
quality = self.quality_var.get()
|
||||
if quality == "high":
|
||||
cmd.extend(["-quality", "90"]) # WebP质量0-100
|
||||
elif quality == "medium":
|
||||
cmd.extend(["-quality", "75"])
|
||||
else:
|
||||
cmd.extend(["-quality", "60"])
|
||||
|
||||
# 设置循环次数 (0表示无限循环)
|
||||
cmd.extend(["-loop", "0"])
|
||||
|
||||
# 添加WebP编码器的特定参数
|
||||
cmd.extend(
|
||||
[
|
||||
"-preset",
|
||||
"picture", # 使用图片预设
|
||||
"-compression_level",
|
||||
"4", # 压缩级别 (0-6)
|
||||
"-lossless",
|
||||
"0", # 使用有损压缩
|
||||
"-metadata",
|
||||
"none", # 不包含元数据
|
||||
]
|
||||
)
|
||||
|
||||
# 添加输出文件
|
||||
cmd.append(output_path)
|
||||
|
||||
# 获取subprocess配置
|
||||
subprocess_config = get_subprocess_config()
|
||||
|
||||
# 打印命令用于调试
|
||||
print("WebP转换命令:", " ".join(cmd))
|
||||
|
||||
# 更新状态显示
|
||||
self.status_label.config(text=f"正在转换... {os.path.basename(input_path)}")
|
||||
self.root.update()
|
||||
|
||||
# 运行转换命令
|
||||
process = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **subprocess_config
|
||||
)
|
||||
_, stderr = process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(f"转换失败: {stderr.decode()}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
print(f"Conversion error: {error_msg}")
|
||||
print(f"Error type: {type(e)}")
|
||||
traceback.print_exc()
|
||||
messagebox.showerror("错误", f"转换失败:\n{error_msg}")
|
||||
return False
|
||||
|
||||
def start_conversion(self):
|
||||
def convert():
|
||||
try:
|
||||
files = self.files_list.get(0, tk.END)
|
||||
if not files:
|
||||
messagebox.showwarning("警告", "请先选择要转换的文件")
|
||||
return
|
||||
|
||||
total = len(files)
|
||||
success_count = 0
|
||||
|
||||
for i, file in enumerate(files):
|
||||
current = i + 1
|
||||
self.status_label.config(
|
||||
text=f"正在转换: {os.path.basename(file)} ({current}/{total})"
|
||||
)
|
||||
if self.convert_to_webp(file): # 使用新的转换函数
|
||||
success_count += 1
|
||||
progress = current / total * 100
|
||||
self.progress["value"] = progress
|
||||
|
||||
self.status_label.config(text=f"转换完成 ({success_count}/{total})")
|
||||
self.convert_btn["state"] = "normal"
|
||||
|
||||
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.progress["value"] = 0
|
||||
Thread(target=convert).start()
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print("程序启动...")
|
||||
|
||||
app = VideoToWebpConverter()
|
||||
app.run()
|
||||
except Exception as e:
|
||||
print(f"程序运行出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc() # 打印详细错误信息
|
||||
sys.exit(1)
|
BIN
icons/favicon.ico
Normal file
BIN
icons/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
icons/logo800.icns
Normal file
BIN
icons/logo800.icns
Normal file
Binary file not shown.
BIN
icons/logo800.jpg
Normal file
BIN
icons/logo800.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# 核心依赖
|
||||
Pillow>=9.0.0
|
17
setup.cfg
Normal file
17
setup.cfg
Normal file
@ -0,0 +1,17 @@
|
||||
[metadata]
|
||||
name = video2webp
|
||||
version = attr: video2webp.__version__
|
||||
description = A GUI tool to convert videos to WEBP
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
author = woodchen
|
||||
author_email = wood@czl.net
|
||||
url = https://github.com/woodchen-ink/video2webp
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
python_requires = >=3.7
|
||||
install_requires =
|
||||
ffmpeg-python>=0.2.0
|
||||
Pillow>=9.0.0
|
||||
tkinterdnd2>=0.3.0
|
42
setup.py
Normal file
42
setup.py
Normal file
@ -0,0 +1,42 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="video-to-webp-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 WEBP converter with GUI",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
keywords="video, webp, converter, ffmpeg, gui",
|
||||
url="https://github.com/yourusername/video-to-webp-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",
|
||||
],
|
||||
)
|
45
video2gif.spec
Normal file
45
video2gif.spec
Normal file
@ -0,0 +1,45 @@
|
||||
# video2webp.spec
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['gui.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('ffmpeg/ffmpeg.exe', 'ffmpeg'),
|
||||
('ffmpeg/ffprobe.exe', 'ffmpeg'),
|
||||
('README.md', '.')
|
||||
],
|
||||
hiddenimports=['ffmpeg', 'ffmpeg-python'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='video2webp',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user