(どこのものかに限らず)
コードは自己責任で利用してくださいね!
環境構築
テキストエディタで作業場所に .bat
を作成し、実行してください。
@echo off
setlocal enabledelayedexpansion
echo "かんたん動画圧縮ソフト セットアップツール"
echo "=========================================="
:: 作業ディレクトリの作成
set WORK_DIR=%USERPROFILE%\video_compressor
if not exist "%WORK_DIR%" mkdir "%WORK_DIR%"
cd /d "%WORK_DIR%"
:: Pythonのインストール確認とダウンロード
python --version > nul 2>&1
if errorlevel 1 (
echo Pythonをダウンロードしています...
curl -o python_installer.exe https://www.python.org/ftp/python/3.11.5/python-3.11.5-amd64.exe
if errorlevel 1 (
echo Pythonのダウンロードに失敗しました
goto :error
)
echo Pythonをインストールしています...
python_installer.exe /quiet InstallAllUsers=1 PrependPath=1
if errorlevel 1 (
echo Pythonのインストールに失敗しました
goto :error
)
)
:: FFMPEGのダウンロードと設定
if not exist "%WORK_DIR%\ffmpeg" (
echo FFMPEGをダウンロードしています...
curl -L -o ffmpeg.zip https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
if errorlevel 1 (
echo FFMPEGのダウンロードに失敗しました
goto :error
)
powershell Expand-Archive ffmpeg.zip .
move ffmpeg-master-latest-win64-gpl ffmpeg
setx PATH "%PATH%;%WORK_DIR%\ffmpeg\bin"
)
:: 必要なライブラリのインストール
echo Pythonライブラリをインストールしています...
python -m pip install --upgrade pip
python -m pip install customtkinter
if errorlevel 1 (
echo ライブラリのインストールに失敗しました
goto :error
)
:: Pythonスクリプトの作成
echo Pythonスクリプトを作成しています...
if not exist "%WORK_DIR%\compressor.py" (
copy nul compressor.py
)
echo "セットアップが完了しました!"
echo "video_compressor フォルダで python compressor.py にスクリプトを書き込んで実行してください"
pause
exit /b 0
:error
echo エラーが発生しました
echo エラーコード: %errorlevel%
pause
exit /b 1
@echo off
setlocal enabledelayedexpansion
echo "かんたん動画圧縮ソフト セットアップツール"
echo "=========================================="
:: 作業ディレクトリの作成
set WORK_DIR=%USERPROFILE%\video_compressor
if not exist "%WORK_DIR%" mkdir "%WORK_DIR%"
cd /d "%WORK_DIR%"
:: Pythonのインストール確認とダウンロード
python --version > nul 2>&1
if errorlevel 1 (
echo Pythonをダウンロードしています...
curl -o python_installer.exe https://www.python.org/ftp/python/3.11.5/python-3.11.5-amd64.exe
if errorlevel 1 (
echo Pythonのダウンロードに失敗しました
goto :error
)
echo Pythonをインストールしています...
python_installer.exe /quiet InstallAllUsers=1 PrependPath=1
if errorlevel 1 (
echo Pythonのインストールに失敗しました
goto :error
)
)
:: FFMPEGのダウンロードと設定
if not exist "%WORK_DIR%\ffmpeg" (
echo FFMPEGをダウンロードしています...
curl -L -o ffmpeg.zip https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
if errorlevel 1 (
echo FFMPEGのダウンロードに失敗しました
goto :error
)
powershell Expand-Archive ffmpeg.zip .
move ffmpeg-master-latest-win64-gpl ffmpeg
setx PATH "%PATH%;%WORK_DIR%\ffmpeg\bin"
)
:: 必要なライブラリのインストール
echo Pythonライブラリをインストールしています...
python -m pip install --upgrade pip
python -m pip install customtkinter
if errorlevel 1 (
echo ライブラリのインストールに失敗しました
goto :error
)
:: Pythonコードを書き込むファイルの作成
set CODE_FILE=%WORK_DIR%\compressor.py
echo # -*- coding: utf-8 -*- > "%CODE_FILE%"
:: 必要なインポート文を追加
(
echo import customtkinter as ctk
echo import subprocess
echo import os
echo from pathlib import Path
echo import threading
echo import tkinter.filedialog as filedialog
echo import re
echo import queue
echo import time
echo class VideoCompressorApp:
echo def __init__(self):
echo self.window = ctk.CTk()
echo self.window.title("かんたん動画圧縮ソフト")
echo self.window.geometry("600x700") # ウィンドウサイズを調整
echo self.main_frame = ctk.CTkFrame(self.window)
echo self.main_frame.pack(pady=20, padx=20, fill="both", expand=True)
echo self.title_label = ctk.CTkLabel(
echo self.main_frame, text="動画圧縮ツール", font=("Helvetica", 24)
echo )
echo self.title_label.pack(pady=(20, 10)) # 下側のpadyを小さくして詰める
echo # 設定フレームをタイトルの直後に配置
echo self.settings_frame = ctk.CTkFrame(self.main_frame)
echo self.settings_frame.pack(pady=(0, 10)) # タイトルとの間隔を調整
echo # エンコーダー選択の変数
echo self.encoder_mode_var = ctk.StringVar(value="cpu_h264")
echo self.crf_value_var = ctk.StringVar(value="28")
echo self.output_path_var = ctk.StringVar(value="")
echo # エンコーダー選択用ラジオボタン
echo encoders_frame = ctk.CTkFrame(self.settings_frame)
echo encoders_frame.pack(pady=5, padx=10, fill="x")
echo ctk.CTkLabel(encoders_frame, text="エンコーダー選択:").pack(pady=2)
echo # ラジオボタンを横並びに変更
echo radio_frame = ctk.CTkFrame(encoders_frame)
echo radio_frame.pack(pady=2)
echo self.cpu_h264_radio = ctk.CTkRadioButton(
echo radio_frame,
echo text="CPU (h.264)",
echo variable=self.encoder_mode_var,
echo value="cpu_h264",
echo )
echo self.cpu_h265_radio = ctk.CTkRadioButton(
echo radio_frame,
echo text="CPU (h.265)",
echo variable=self.encoder_mode_var,
echo value="cpu_h265",
echo )
echo self.gpu_hevc_radio = ctk.CTkRadioButton(
echo radio_frame,
echo text="GPU (HEVC)",
echo variable=self.encoder_mode_var,
echo value="hevc_nvenc",
echo )
echo self.cpu_h264_radio.pack(side="left", padx=10)
echo self.cpu_h265_radio.pack(side="left", padx=10)
echo self.gpu_hevc_radio.pack(side="left", padx=10)
echo # CRF値の入力とツールチップ
echo crf_frame = ctk.CTkFrame(self.settings_frame)
echo crf_frame.pack(pady=5, padx=10, fill="x")
echo crf_label = ctk.CTkLabel(crf_frame, text="CRF値 (0-51):")
echo crf_label.pack(side="left", padx=5)
echo self.crf_entry = ctk.CTkEntry(
echo crf_frame, textvariable=self.crf_value_var, width=50
echo )
echo self.crf_entry.pack(side="left", padx=5)
echo crf_help = ctk.CTkLabel(crf_frame, text="?", width=20)
echo crf_help.pack(side="left", padx=5)
echo # 出力先ディレクトリ選択
echo output_frame = ctk.CTkFrame(self.settings_frame)
echo output_frame.pack(pady=5, padx=10, fill="x")
echo ctk.CTkLabel(output_frame, text="出力先:").pack(side="left", padx=5)
echo self.output_path_entry = ctk.CTkEntry(
echo output_frame, textvariable=self.output_path_var, width=300
echo )
echo self.output_path_entry.pack(side="left", padx=5)
echo ctk.CTkButton(
echo output_frame, text="参照", width=50, command=self.select_output_directory
echo ).pack(side="left", padx=5)
echo # ツールチップの設定
echo self.tooltip_window = None
echo crf_help.bind("<Enter>", self.show_crf_tooltip)
echo crf_help.bind("<Leave>", self.hide_crf_tooltip)
echo # ファイル選択ボタンを設定の下に配置
echo self.select_button = ctk.CTkButton(
echo self.main_frame, text="動画ファイルを選択", command=self.select_files
echo )
echo self.select_button.pack(pady=10)
echo # ファイルリストを表示するテキストボックス
echo self.file_list = ctk.CTkTextbox(self.main_frame, height=100, width=500)
echo self.file_list.pack(pady=10)
echo # 全体のプログレスバー
echo self.total_progress_label = ctk.CTkLabel(self.main_frame, text="全体の進捗:")
echo self.total_progress_label.pack(pady=(10, 0))
echo self.total_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
echo self.total_progress_bar.pack(pady=(0, 10))
echo self.total_progress_bar.set(0)
echo # 現在のファイルのプログレスバー
echo self.current_progress_label = ctk.CTkLabel(
echo self.main_frame, text="現在のファイルの進捗:"
echo )
echo self.current_progress_label.pack(pady=(10, 0))
echo self.current_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
echo self.current_progress_bar.pack(pady=(0, 10))
echo self.current_progress_bar.set(0)
echo # FFMPEGの出力表示用テキストボックス
echo self.output_label = ctk.CTkLabel(self.main_frame, text="FFMPEG出力:")
echo self.output_label.pack(pady=(10, 0))
echo self.output_text = ctk.CTkTextbox(self.main_frame, height=100, width=500)
echo self.output_text.pack(pady=10)
echo # 状態表示ラベル
echo self.status_label = ctk.CTkLabel(self.main_frame, text="")
echo self.status_label.pack(pady=10)
echo self.output_directory = None # この変数は引き続き使用
echo self.selected_files = []
echo # キューの初期化
echo self.message_queue = queue.Queue()
echo self.processing = False
echo # 定期的なGUI更新用のメソッド呼び出し
echo self.window.after(100, self.update_gui)
echo def select_files(self):
echo # 複数ファイル選択に対応
echo files = filedialog.askopenfilenames(filetypes=[("動画ファイル", "*.mp4 *.mov")])
echo if files:
echo self.selected_files = files
echo self.file_list.delete("1.0", "end")
echo for file in files:
echo self.file_list.insert("end", f"{file}\n")
echo # ファイル選択後すぐに圧縮開始
echo self.start_compression()
echo def select_output_directory(self):
echo directory = filedialog.askdirectory()
echo if directory:
echo self.output_directory = directory
echo self.status_label.configure(text=f"出力先: {directory}")
echo def start_compression(self):
echo # 圧縮処理を別スレッドで実行
echo self.select_button.configure(state="disabled")
echo self.output_text.delete("1.0", "end") # 出力テキストをクリア
echo self.settings_frame.pack_forget() # 動画圧縮開始時に設定を隠す
echo threading.Thread(target=self.compress_videos, daemon=True).start()
echo def update_output(self, text):
echo self.output_text.insert("end", text + "\n")
echo self.output_text.see("end") # 最新の出力が見えるようにスクロール
echo def update_gui(self):
echo """GUIの更新を担当するメソッド"""
echo try:
echo # キューからメッセージを取り出して処理
echo while not self.message_queue.empty():
echo msg_type, msg_data = self.message_queue.get_nowait()
echo if msg_type == "output":
echo self.output_text.insert("end", msg_data + "\n")
echo self.output_text.see("end")
echo elif msg_type == "progress":
echo file_index, progress = msg_data
echo self.total_progress_bar.set(file_index / len(self.selected_files))
echo self.current_progress_bar.set(progress)
echo elif msg_type == "status":
echo self.status_label.configure(text=msg_data)
echo elif msg_type == "error":
echo self.status_label.configure(text=f"エラー: {msg_data}")
echo self.output_text.insert("end", f"エラー: {msg_data}\n")
echo self.output_text.see("end")
echo except queue.Empty:
echo pass
echo # GUI更新を継続
echo self.window.after(100, self.update_gui)
echo def compress_videos(self):
echo """FFMPEGの処理を担当するメソッド"""
echo self.processing = True
echo total_files = len(self.selected_files)
echo for index, file_path in enumerate(self.selected_files):
echo try:
echo input_path = Path(file_path)
echo if self.output_directory:
echo output_path = (
echo Path(self.output_directory) / f"Compressed_{input_path.name}"
echo )
echo else:
echo downloads_path = Path.home() / "Downloads"
echo output_path = downloads_path / f"Compressed_{input_path.name}"
echo # 動画の長さを取得
echo duration_cmd = [
echo "ffprobe",
echo "-v",
echo "error",
echo "-show_entries",
echo "format=duration",
echo "-of",
echo "default=noprint_wrappers=1:nokey=1",
echo str(file_path),
echo ]
echo duration = float(subprocess.check_output(duration_cmd).decode().strip())
echo # FFMPEGコマンドの実行
echo command = ["ffmpeg", "-i", str(file_path), "-map_metadata", "0"]
echo # エンコーダーの設定
echo encoder_mode = self.encoder_mode_var.get()
echo if (encoder_mode == "cpu_h264"):
echo command += ["-c:v", "libx264"]
echo elif (encoder_mode == "cpu_h265"):
echo command += ["-c:v", "libx265"]
echo else: # hevc_nvenc
echo command += ["-c:v", "hevc_nvenc"]
echo # CRF値の設定
echo command += ["-crf", self.crf_value_var.get()]
echo if self.output_path_var.get():
echo # 出力名が指定されていればファイル名に使う
echo output_name = self.output_path_var.get()
echo if "." not in output_name:
echo output_name += f"_{input_path.name}"
echo output_path = (
echo Path(self.output_directory or downloads_path) / output_name
echo )
echo command += [
echo "-progress",
echo "pipe:1",
echo str(output_path),
echo ]
echo process = subprocess.Popen(
echo command,
echo stdout=subprocess.PIPE,
echo stderr=subprocess.PIPE,
echo universal_newlines=True,
echo bufsize=1,
echo encoding="utf-8",
echo errors="replace",
echo )
echo # 標準エラー出力の監視
echo def monitor_stderr():
echo for line in process.stderr:
echo self.message_queue.put(("output", line.strip()))
echo stderr_thread = threading.Thread(target=monitor_stderr, daemon=True)
echo stderr_thread.start()
echo # 進捗の監視
echo last_progress = 0
echo while True:
echo line = process.stdout.readline()
echo if not line:
echo break
echo if "out_time_ms=" in line:
echo try:
echo time_str = line.split("=")[1].strip()
echo if time_str != "N/A":
echo time_ms = int(time_str) / 1000000
echo progress = min(time_ms / duration, 1.0)
echo if (
echo abs(progress - last_progress) >= 0.01
echo ): # 1%以上の変化がある場合のみ更新
echo self.message_queue.put(
echo ("progress", (index, progress))
echo )
echo last_progress = progress
echo except (ValueError, IndexError):
echo continue
echo process.wait()
echo stderr_thread.join()
echo if process.returncode != 0:
echo self.message_queue.put(
echo ("error", f"{input_path.name}の処理中にエラーが発生しました")
echo )
echo # 現在のファイルの完了を通知
echo self.message_queue.put(("progress", (index + 1, 0)))
echo self.message_queue.put(("status", f"処理中: {index + 1}/{total_files}"))
echo except Exception as e:
echo self.message_queue.put(("error", str(e)))
echo # 処理完了の通知
echo self.message_queue.put(("status", "すべての圧縮が完了しました!"))
echo self.message_queue.put(("output", "処理が完了しました!"))
echo self.processing = False
echo self.window.after(0, self.compression_completed)
echo def show_crf_tooltip(self, event):
echo tooltip_text = (
echo "CRF (Constant Rate Factor)は動画の品質を制御します。\n"
echo "値が小さいほど高品質、大きいほど低品質になります。\n"
echo "推奨値:\n"
echo "- h.264: 18-28\n"
echo "- h.265: 22-32\n"
echo "※GPUエンコードの場合は効果が異なる場合があります"
echo )
echo x = event.widget.winfo_rootx() + event.widget.winfo_width()
echo y = event.widget.winfo_rooty()
echo self.tooltip_window = ctk.CTkToplevel()
echo self.tooltip_window.wm_overrideredirect(True)
echo self.tooltip_window.geometry(f"+{x}+{y}")
echo label = ctk.CTkLabel(self.tooltip_window, text=tooltip_text, justify="left")
echo label.pack(padx=5, pady=5)
echo def hide_crf_tooltip(self, event):
echo if self.tooltip_window:
echo self.tooltip_window.destroy()
echo self.tooltip_window = None
echo def compression_completed(self):
echo self.total_progress_bar.set(1)
echo self.current_progress_bar.set(0)
echo self.status_label.configure(text="すべての圧縮が完了しました!")
echo self.select_button.configure(state="normal")
echo self.update_output("処理が完了しました!")
echo self.settings_frame.pack(pady=5) # 処理完了時に再表示
echo def run(self):
echo self.window.mainloop()
echo if __name__ == "__main__":
echo app = VideoCompressorApp()
echo app.run()
) > video_compressor.py
echo "Pythonファイルの生成が完了しました"
echo "セットアップが完了しました!"
echo "video_compressor フォルダで python compressor.py を実行してください"
pause
exit /b 0
:error
echo エラーが発生しました
echo エラーコード: %errorlevel%
pause
exit /b 1
コード(自己責任で利用してくださいね!)
import customtkinter as ctk
import subprocess
import os
from pathlib import Path
import threading
import tkinter.filedialog as filedialog
import re
import queue
import time
class VideoCompressorApp:
def __init__(self):
self.window = ctk.CTk()
self.window.title("かんたん動画圧縮ソフト")
self.window.geometry("600x700") # ウィンドウサイズを調整
self.main_frame = ctk.CTkFrame(self.window)
self.main_frame.pack(pady=20, padx=20, fill="both", expand=True)
self.title_label = ctk.CTkLabel(
self.main_frame, text="動画圧縮ツール", font=("Helvetica", 24)
)
self.title_label.pack(pady=(20, 10)) # 下側のpadyを小さくして詰める
# 設定フレームをタイトルの直後に配置
self.settings_frame = ctk.CTkFrame(self.main_frame)
self.settings_frame.pack(pady=(0, 10)) # タイトルとの間隔を調整
# エンコーダー選択の変数
self.encoder_mode_var = ctk.StringVar(value="cpu_h264")
self.crf_value_var = ctk.StringVar(value="28")
self.output_path_var = ctk.StringVar(value="")
# エンコーダー選択用ラジオボタン
encoders_frame = ctk.CTkFrame(self.settings_frame)
encoders_frame.pack(pady=5, padx=10, fill="x")
ctk.CTkLabel(encoders_frame, text="エンコーダー選択:").pack(pady=2)
# ラジオボタンを横並びに変更
radio_frame = ctk.CTkFrame(encoders_frame)
radio_frame.pack(pady=2)
self.cpu_h264_radio = ctk.CTkRadioButton(
radio_frame,
text="CPU (h.264)",
variable=self.encoder_mode_var,
value="cpu_h264",
)
self.cpu_h265_radio = ctk.CTkRadioButton(
radio_frame,
text="CPU (h.265)",
variable=self.encoder_mode_var,
value="cpu_h265",
)
self.gpu_hevc_radio = ctk.CTkRadioButton(
radio_frame,
text="GPU (HEVC)",
variable=self.encoder_mode_var,
value="hevc_nvenc",
)
self.cpu_h264_radio.pack(side="left", padx=10)
self.cpu_h265_radio.pack(side="left", padx=10)
self.gpu_hevc_radio.pack(side="left", padx=10)
# CRF値の入力とツールチップ
crf_frame = ctk.CTkFrame(self.settings_frame)
crf_frame.pack(pady=5, padx=10, fill="x")
crf_label = ctk.CTkLabel(crf_frame, text="CRF値 (0-51):")
crf_label.pack(side="left", padx=5)
self.crf_entry = ctk.CTkEntry(
crf_frame, textvariable=self.crf_value_var, width=50
)
self.crf_entry.pack(side="left", padx=5)
crf_help = ctk.CTkLabel(crf_frame, text="?", width=20)
crf_help.pack(side="left", padx=5)
# 出力先ディレクトリ選択
output_frame = ctk.CTkFrame(self.settings_frame)
output_frame.pack(pady=5, padx=10, fill="x")
ctk.CTkLabel(output_frame, text="出力先:").pack(side="left", padx=5)
self.output_path_entry = ctk.CTkEntry(
output_frame, textvariable=self.output_path_var, width=300
)
self.output_path_entry.pack(side="left", padx=5)
ctk.CTkButton(
output_frame, text="参照", width=50, command=self.select_output_directory
).pack(side="left", padx=5)
# ツールチップの設定
self.tooltip_window = None
crf_help.bind("<Enter>", self.show_crf_tooltip)
crf_help.bind("<Leave>", self.hide_crf_tooltip)
# ファイル選択ボタンを設定の下に配置
self.select_button = ctk.CTkButton(
self.main_frame, text="動画ファイルを選択", command=self.select_files
)
self.select_button.pack(pady=10)
# ファイルリストを表示するテキストボックス
self.file_list = ctk.CTkTextbox(self.main_frame, height=100, width=500)
self.file_list.pack(pady=10)
# 全体のプログレスバー
self.total_progress_label = ctk.CTkLabel(self.main_frame, text="全体の進捗:")
self.total_progress_label.pack(pady=(10, 0))
self.total_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
self.total_progress_bar.pack(pady=(0, 10))
self.total_progress_bar.set(0)
# 現在のファイルのプログレスバー
self.current_progress_label = ctk.CTkLabel(
self.main_frame, text="現在のファイルの進捗:"
)
self.current_progress_label.pack(pady=(10, 0))
self.current_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
self.current_progress_bar.pack(pady=(0, 10))
self.current_progress_bar.set(0)
# FFMPEGの出力表示用テキストボックス
self.output_label = ctk.CTkLabel(self.main_frame, text="FFMPEG出力:")
self.output_label.pack(pady=(10, 0))
self.output_text = ctk.CTkTextbox(self.main_frame, height=100, width=500)
self.output_text.pack(pady=10)
# 状態表示ラベル
self.status_label = ctk.CTkLabel(self.main_frame, text="")
self.status_label.pack(pady=10)
self.output_directory = None # この変数は引き続き使用
self.selected_files = []
# キューの初期化
self.message_queue = queue.Queue()
self.processing = False
# 定期的なGUI更新用のメソッド呼び出し
self.window.after(100, self.update_gui)
def select_files(self):
# 複数ファイル選択に対応
files = filedialog.askopenfilenames(filetypes=[("動画ファイル", "*.mp4 *.mov")])
if files:
self.selected_files = files
self.file_list.delete("1.0", "end")
for file in files:
self.file_list.insert("end", f"{file}\n")
# ファイル選択後すぐに圧縮開始
self.start_compression()
def select_output_directory(self):
directory = filedialog.askdirectory()
if directory:
self.output_directory = directory
self.status_label.configure(text=f"出力先: {directory}")
def start_compression(self):
# 圧縮処理を別スレッドで実行
self.select_button.configure(state="disabled")
self.output_text.delete("1.0", "end") # 出力テキストをクリア
self.settings_frame.pack_forget() # 動画圧縮開始時に設定を隠す
threading.Thread(target=self.compress_videos, daemon=True).start()
def update_output(self, text):
self.output_text.insert("end", text + "\n")
self.output_text.see("end") # 最新の出力が見えるようにスクロール
def update_gui(self):
"""GUIの更新を担当するメソッド"""
try:
# キューからメッセージを取り出して処理
while not self.message_queue.empty():
msg_type, msg_data = self.message_queue.get_nowait()
if msg_type == "output":
self.output_text.insert("end", msg_data + "\n")
self.output_text.see("end")
elif msg_type == "progress":
file_index, progress = msg_data
self.total_progress_bar.set(file_index / len(self.selected_files))
self.current_progress_bar.set(progress)
elif msg_type == "status":
self.status_label.configure(text=msg_data)
elif msg_type == "error":
self.status_label.configure(text=f"エラー: {msg_data}")
self.output_text.insert("end", f"エラー: {msg_data}\n")
self.output_text.see("end")
except queue.Empty:
pass
# GUI更新を継続
self.window.after(100, self.update_gui)
def compress_videos(self):
"""FFMPEGの処理を担当するメソッド"""
self.processing = True
total_files = len(self.selected_files)
for index, file_path in enumerate(self.selected_files):
try:
input_path = Path(file_path)
if self.output_directory:
output_path = (
Path(self.output_directory) / f"Compressed_{input_path.name}"
)
else:
downloads_path = Path.home() / "Downloads"
output_path = downloads_path / f"Compressed_{input_path.name}"
# 動画の長さを取得
duration_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(file_path),
]
duration = float(subprocess.check_output(duration_cmd).decode().strip())
# FFMPEGコマンドの実行
command = ["ffmpeg", "-i", str(file_path), "-map_metadata", "0"]
# エンコーダーの設定
encoder_mode = self.encoder_mode_var.get()
if (encoder_mode == "cpu_h264"):
command += ["-c:v", "libx264"]
elif (encoder_mode == "cpu_h265"):
command += ["-c:v", "libx265"]
else: # hevc_nvenc
command += ["-c:v", "hevc_nvenc"]
# CRF値の設定
command += ["-crf", self.crf_value_var.get()]
if self.output_path_var.get():
# 出力名が指定されていればファイル名に使う
output_name = self.output_path_var.get()
if "." not in output_name:
output_name += f"_{input_path.name}"
output_path = (
Path(self.output_directory or downloads_path) / output_name
)
command += [
"-progress",
"pipe:1",
str(output_path),
]
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1,
encoding="utf-8",
errors="replace",
)
# 標準エラー出力の監視
def monitor_stderr():
for line in process.stderr:
self.message_queue.put(("output", line.strip()))
stderr_thread = threading.Thread(target=monitor_stderr, daemon=True)
stderr_thread.start()
# 進捗の監視
last_progress = 0
while True:
line = process.stdout.readline()
if not line:
break
if "out_time_ms=" in line:
try:
time_str = line.split("=")[1].strip()
if time_str != "N/A":
time_ms = int(time_str) / 1000000
progress = min(time_ms / duration, 1.0)
if (
abs(progress - last_progress) >= 0.01
): # 1%以上の変化がある場合のみ更新
self.message_queue.put(
("progress", (index, progress))
)
last_progress = progress
except (ValueError, IndexError):
continue
process.wait()
stderr_thread.join()
if process.returncode != 0:
self.message_queue.put(
("error", f"{input_path.name}の処理中にエラーが発生しました")
)
# 現在のファイルの完了を通知
self.message_queue.put(("progress", (index + 1, 0)))
self.message_queue.put(("status", f"処理中: {index + 1}/{total_files}"))
except Exception as e:
self.message_queue.put(("error", str(e)))
# 処理完了の通知
self.message_queue.put(("status", "すべての圧縮が完了しました!"))
self.message_queue.put(("output", "処理が完了しました!"))
self.processing = False
self.window.after(0, self.compression_completed)
def show_crf_tooltip(self, event):
tooltip_text = (
"CRF (Constant Rate Factor)は動画の品質を制御します。\n"
"値が小さいほど高品質、大きいほど低品質になります。\n"
"推奨値:\n"
"- h.264: 18-28\n"
"- h.265: 22-32\n"
"※GPUエンコードの場合は効果が異なる場合があります"
)
x = event.widget.winfo_rootx() + event.widget.winfo_width()
y = event.widget.winfo_rooty()
self.tooltip_window = ctk.CTkToplevel()
self.tooltip_window.wm_overrideredirect(True)
self.tooltip_window.geometry(f"+{x}+{y}")
label = ctk.CTkLabel(self.tooltip_window, text=tooltip_text, justify="left")
label.pack(padx=5, pady=5)
def hide_crf_tooltip(self, event):
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
def compression_completed(self):
self.total_progress_bar.set(1)
self.current_progress_bar.set(0)
self.status_label.configure(text="すべての圧縮が完了しました!")
self.select_button.configure(state="normal")
self.update_output("処理が完了しました!")
self.settings_frame.pack(pady=5) # 処理完了時に再表示
def run(self):
self.window.mainloop()
if __name__ == "__main__":
app = VideoCompressorApp()
app.run()
経緯
- 契約しているクラウドストレージの容量が足りなくなってきた。
- 確認すると、大量の動画があることに気付いた。
- でも消したくないし…と思った。
- 圧縮してローカルにおいておくっていうのも、画質落ちそうで嫌だし、いつでも確認できない。
- そういえばffmpegラッパーのソフトを売ってちょっと燃えた人いたなと思い出す。
- 無料で圧縮できそうだなと思い、ChatGPTやパープレと壁打ち。
- 色々調整してもらいつつ、CustomTkinterで実装してもらった。コードは一ミリも書いていない。ははは。
- 設定変更(ffmpegコマンド生成)
- 複数ファイル逐次実行(さすがに並列だと重すぎて意味ないんじゃないかと思って。)
- なぜかメタデータをコピーできない。なんで。
コメントを残す