開発の経緯
- ダウンロードした動画を見る時間がない
できること
- 動画から任意の枚数(グリッド数)の画像を抽出して一枚のアーカイブ画像として動画と同じディレクトリに保存。PPTファイルも。ただし縦長画像はPPT作りません
- アーカイブ画像をパワポに張り込んでハイパーリンクを追加
注意点
- HDDを使っている方はウインドウの右上の⚙マークから環境設定のウインドウを開き、シングルスレッド処理を選んでください。マルチスレッド処理がデフォです。HDDでマルチスレッドを使うと処理がバチクソ遅くなります。
- 環境設定の#を削除にチェックするとファイル名の特殊文字が置換されるので注意して下さい。この機能はファイル名に#やハイパーリンクに使えない文字が含まれている場合にリンクから動画を開けないので追加したものです。通常はOFFにしてください!エスケープする方法がわかりませんでした・・・
このような画像がPPTファイルに張り込まれてハイパーリンクが付きます
抽出フレーム数を最大にするとこんな感じ。
ということで以下が本体コードです。
Ver1.4.py
# Ver1.4
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from pptx import Presentation
from PIL import Image
import os
import configparser
from pptx.util import Inches
import logging
import re
import threading
import queue
import subprocess
import json
from moviepy.editor import VideoFileClip
import time
import psutil
import gc
cache_lock = threading.Lock()
ppt_save_lock = threading.Lock()
def natural_sort_key(s):
return [int(text) if text.isdigit() else text.lower() for text in re.split('([0-9]+)', s)]
def load_video_details_cache():
try:
with open("video_details_cache.json", "r") as cache_file:
return json.load(cache_file)
except (FileNotFoundError, json.JSONDecodeError, IOError) as e:
logging.exception(f"キャッシュファイルの読み込み中にエラーが発生しました: {e}")
return {}
def extract_video_details_thread(video_paths, progress_queue, folder_path, video_details_cache):
for video_path in video_paths:
try:
video_name = os.path.basename(video_path)
if video_name in video_details_cache:
file_size, video_duration_min, video_duration_sec = video_details_cache[video_name]
progress_queue.put(("detail_extraction_success", video_path, folder_path, file_size, video_duration_min, video_duration_sec))
else:
file_info = os.stat(video_path)
file_size = file_info.st_size / (1024 * 1024)
video_duration = get_video_duration(video_path)
video_duration_min = int(video_duration // 60)
video_duration_sec = int(video_duration % 60)
video_details_cache[video_name] = (file_size, video_duration_min, video_duration_sec)
progress_queue.put(("detail_extraction_success", video_path, folder_path, file_size, video_duration_min, video_duration_sec, video_details_cache))
except Exception as e:
logging.exception(f"動画 {video_path} の詳細情報の取得中にエラーが発生しました: {e}")
progress_queue.put(("detail_extraction_failed", video_path, folder_path))
def get_video_duration(video_path):
try:
clip = VideoFileClip(video_path)
duration = clip.duration
clip.close()
return duration
except Exception as e:
logging.exception(f"動画の再生時間の取得中にエラーが発生しました: {e}")
return None
def save_video_details_cache(cache):
with cache_lock:
try:
with open("video_details_cache.json", "w") as cache_file:
json.dump(cache, cache_file)
except IOError as e:
logging.exception(f"キャッシュファイルの保存中にエラーが発生しました: {e}")
class TextHandler(logging.Handler):
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget
def emit(self, record):
msg = self.format(record)
color = None
if record.levelno == logging.INFO:
if "ファイル名を変更しました" in msg:
color = "#c39163" # ファイル名変更のログメッセージを赤色に設定
elif "全ての処理が完了しました。" in msg:
color = "#c39163" # 処理完了のログメッセージを赤色に設定
elif "ファイル名の変更は不要です" in msg:
color = "#abb2bf" # ファイル名が変更されない場合のログメッセージを青色に設定
else:
color = '#abb2bf' # その他のINFOレベルのログメッセージを青色に設定
elif record.levelno == logging.WARNING:
color = '#61aeee'
elif record.levelno == logging.ERROR:
color = '#c39163'
self.text_widget.configure(state='normal')
if color:
self.text_widget.insert(tk.END, msg + '\n', color)
self.text_widget.tag_config(color, foreground=color)
else:
self.text_widget.insert(tk.END, msg + '\n')
self.text_widget.configure(state='disabled')
self.text_widget.yview(tk.END)
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.master.geometry("320x500") # メインウィンドウのサイズを設定
self.master.resizable(False, False) # メインウィンドウのサイズ変更を禁止
self.remove_hashtags_var = tk.BooleanVar(value=True)# ハッシュタグ削除
self.config = configparser.ConfigParser()
self.grid_size_var = tk.StringVar()
self.sort_by_name_var = tk.BooleanVar()
self.add_details_var = tk.BooleanVar(value=True)
self.single_thread_var = tk.BooleanVar() # 追加
self.max_threads = os.cpu_count() or 4
self.load_config()
self.create_widgets()
self.progress_queue = queue.Queue()
self.master.protocol("WM_DELETE_WINDOW", self.on_close)
def remove_special_characters_from_filenames(self, folder_path):
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.lower().endswith(('.mp4', '.avi', '.mov')): # Process video files only
file_path = os.path.join(root, file)
new_file_name = re.sub(r'#', '#', file) # Replace "#" with "#"
new_file_name = re.sub(r'[^\w.\s#-]+', '_', new_file_name) # Replace consecutive special characters with a single underscore
new_file_name = re.sub(r'_+', '_', new_file_name) # Replace consecutive underscores with a single underscore
new_file_name = re.sub(r'\s+', ' ', new_file_name) # Replace consecutive spaces with a single space
new_file_path = os.path.join(root, new_file_name)
# Handle duplicate video file names by adding a sequence number
if new_file_name != file:
counter = 1
base_name, extension = os.path.splitext(new_file_name)
while os.path.exists(new_file_path):
new_file_name = f"{base_name}_{counter:03d}{extension}"
new_file_path = os.path.join(root, new_file_name)
counter += 1
# Rename the file if necessary
if new_file_path != file_path:
os.rename(file_path, new_file_path)
logging.info(f"ファイル名を変更しました。: {file} -> {new_file_name}") # ログメッセージを記録
else:
logging.info(f"ファイル名の変更は不要です。: {file}") # ファイル名が変更されない場合のログメッセージを記録
def get_video_details(self, video_path):
file_size = os.path.getsize(video_path) / (1024 * 1024) # ファイルサイズ(MB)
duration = int(os.popen(f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{video_path}"').read().strip())
minutes, seconds = divmod(duration, 60)
return file_size, minutes, seconds
def update_status(self, current=None, total=None, folder_path=""):
if current is not None and total is not None:
progress_status = f"処理中: {current}/{total}"
else:
progress_status = "待機中"
self.status_label.config(text=f"フォルダ{progress_status}")
if len(folder_path) > 50:
folder_path = "..." + folder_path[-47:]
self.folder_label.config(text=folder_path)
def on_close(self):
if messagebox.askokcancel("終了", "アプリケーションを終了しますか?"):
# 実行中のスレッドを終了
for thread in threading.enumerate():
if thread != threading.current_thread():
thread.join(timeout=1)
# VideoFileClipインスタンスを閉じる
for obj in gc.get_objects():
if isinstance(obj, VideoFileClip):
obj.close()
# psutilを使用して、現在のプロセスに関連するリソースを解放
process = psutil.Process()
for proc in process.children(recursive=True):
proc.kill()
process.kill()
# すべてのウィンドウを閉じる
self.master.destroy()
def create_widgets(self):
settings_icon = tk.Label(self, text="⚙️")
settings_icon.bind("<Button-1>", lambda event: self.open_settings_dialog())
settings_icon.pack(side=tk.TOP, pady=0, anchor=tk.E)
self.select_folder_button = tk.Button(self, text="フォルダを選択", command=self.select_folder, width=20, height=2)
self.select_folder_button.pack(pady=10, padx=10, expand=True, anchor=tk.CENTER)
"""
self.sort_by_name_checkbutton = tk.Checkbutton(self, text="名前順でソート", variable=self.sort_by_name_var)
self.sort_by_name_checkbutton.pack(side=tk.TOP, pady=5)
self.add_details_checkbutton = tk.Checkbutton(self, text="詳細情報を追加", variable=self.add_details_var)
self.add_details_checkbutton.pack(side=tk.TOP, pady=5)
self.single_thread_var = tk.BooleanVar()
self.single_thread_checkbutton = tk.Checkbutton(self, text="シングルスレッド処理", variable=self.single_thread_var)
self.single_thread_checkbutton.pack(side=tk.TOP, pady=5)
"""
values1 = ["2x2", "3x3", "4x4", "5x5", "6x6", "7x7"]
values2 = ["2x6", "3x9", "4x12", "5x15", "6x18", "7x21"]
values = values1 + ["─────────"] + values2
self.grid_size_menu = ttk.Combobox(self, textvariable=self.grid_size_var, values=values)
self.grid_size_menu.pack(side=tk.TOP, pady=5)
self.execute_button = tk.Button(self, text="実行", command=self.execute, width=20, height=2)
self.execute_button.pack(pady=10, padx=10)
self.progress_bar = ttk.Progressbar(self, orient="horizontal", length=280, mode="determinate")
self.progress_bar.pack(pady=10, padx=20)
self.detail_extraction_progress = ttk.Progressbar(self, orient="horizontal", length=280, mode="determinate")
self.detail_extraction_progress.pack(pady=10, padx=20)
#self.folder_progress_bar = ttk.Progressbar(self, orient="horizontal", length=300, mode="determinate")
#self.folder_progress_bar.pack(pady=5, padx=10)
self.video_folder_path_label = tk.Label(self, text="パス: なし")
self.video_folder_path_label.pack(pady=1, padx=10)
self.selected_videos_label = tk.Label(self, text="フォルダを選択してください: 0/0")
self.selected_videos_label.pack(pady=1, padx=10)
self.status_label = tk.Label(self, text="ステータス: 待機中")
self.status_label.pack(pady=1, padx=10)
self.folder_label = tk.Label(self, text="")
self.folder_label.pack(pady=1, padx=10)
self.log_text = tk.Text(self, wrap=tk.WORD, width=40, height=10, state='disabled', bg="#333333", fg="white")
self.log_text.pack(side=tk.BOTTOM, pady=(10, 20), padx=10)
self.video_paths = []
def open_settings_dialog(self):
if not hasattr(self, 'settings_dialog') or not self.settings_dialog.winfo_exists():
self.settings_dialog = tk.Toplevel(self)
self.settings_dialog.title("環境設定")
self.settings_dialog.geometry("450x200") # 環境設定ウィンドウのサイズを設定
self.settings_dialog.resizable(False, False) # 環境設定ウィンドウのサイズ変更を禁止
sort_by_name_checkbutton = tk.Checkbutton(self.settings_dialog, text="名前順でソート", variable=self.sort_by_name_var)
sort_by_name_checkbutton.pack(side=tk.TOP, pady=5, anchor=tk.W)
add_details_checkbutton = tk.Checkbutton(self.settings_dialog, text="詳細情報を追加", variable=self.add_details_var)
add_details_checkbutton.pack(side=tk.TOP, pady=5, anchor=tk.W)
single_thread_checkbutton = tk.Checkbutton(self.settings_dialog, text="シングルスレッド処理", variable=self.single_thread_var)
single_thread_checkbutton.pack(side=tk.TOP, pady=5, anchor=tk.W)
remove_hashtags_checkbutton = tk.Checkbutton(self.settings_dialog, text="#を削除", variable=self.remove_hashtags_var)
remove_hashtags_checkbutton.pack(side=tk.TOP, pady=5, anchor=tk.W)
ok_button = tk.Button(self.settings_dialog, text="OK", command=lambda: [self.save_config(), self.settings_dialog.destroy()])
ok_button.pack(side=tk.TOP, pady=5)
else:
self.settings_dialog.deiconify() # 環境設定ウィンドウを表示
def load_config(self):
self.config.read('settings.ini')
self.video_folder_path = self.config.get('Paths', 'VideoFolder', fallback=None)
self.ppt_path = self.config.get('Paths', 'PPTPath', fallback=None)
grid_size_value = self.config.get('Settings', 'GridSize', fallback='4x4')
self.grid_size_var.set(grid_size_value)
self.sort_by_name_var.set(self.config.getboolean('Settings', 'SortByName', fallback=False))
self.add_details_var.set(self.config.getboolean('Settings', 'AddDetails', fallback=True))
self.single_thread_var.set(self.config.getboolean('Settings', 'SingleThread', fallback=False))
self.remove_hashtags_var.set(self.config.getboolean('Settings', 'RemoveHashtags', fallback=True))
def save_config(self):
self.config['Paths'] = {
'VideoFolder': self.video_folder_path if self.video_folder_path else '',
'PPTPath': self.ppt_path if self.ppt_path else ''
}
self.config['Settings'] = {
'RemoveHashtags': str(self.remove_hashtags_var.get()),
'GridSize': self.grid_size_var.get(),
'SortByName': self.sort_by_name_var.get(),
'AddDetails': self.add_details_var.get(),
'SingleThread': self.single_thread_var.get()
}
with open('settings.ini', 'w') as configfile:
self.config.write(configfile)
def select_folder(self):
folder_path = filedialog.askdirectory(title="フォルダを選択")
if folder_path:
self.video_folder_path = folder_path
self.video_folder_path_label.config(text=f"パス: {folder_path}")
self.video_paths = self.get_all_video_files(folder_path)
self.all_folders = self.get_all_folders(folder_path)
self.total_folders = len(self.all_folders)
self.progress_bar["value"] = 0
self.detail_extraction_progress["value"] = 0
# self.total_progress_bar["value"] = 0
# self.folder_progress_bar["value"] = 0
self.total_videos_count = self.count_total_videos(folder_path)
self.progress_bar["maximum"] = self.total_videos_count # 追加
self.processed_videos_count = 0
if not self.video_paths:
messagebox.showerror("エラー", "動画ファイルが見つかりませんでした。")
return
self.selected_videos_label.config(text=f"総動画数: 0/{len(self.video_paths)}")
# self.selected_videos_label.config(text=f"総動画数: {len(self.video_paths)}")
self.save_config()
def get_all_video_files(self, folder_path):
supported_formats = {'.mp4', '.avi', '.mov'}
video_files = []
for root, dirs, files in os.walk(folder_path):
for file in files:
if any(file.lower().endswith(ext) for ext in supported_formats):
video_files.append(os.path.join(root, file))
else:
logging.warning(f"スキップされたファイル: {file}")
if self.sort_by_name_var.get():
video_files.sort(key=natural_sort_key)
return video_files
def execute(self):
if not self.video_paths:
messagebox.showerror("エラー", "動画を選択してください")
return
if self.remove_hashtags_var.get():
self.remove_special_characters_from_filenames(self.video_folder_path)
# self.total_progress_bar["value"] = 0
# self.folder_progress_bar["value"] = 0
self.update_status(folder_path="処理を開始しています...")
thread = threading.Thread(target=self.process_videos)
thread.start()
self.master.after(100, self.check_thread, thread)
def check_thread(self, thread):
if thread.is_alive():
self.master.after(100, self.check_thread, thread)
else:
if not self.error_occurred:
self.update_status("待機中")
self.reset_button_states()
def process_video(self, video_path, sub_presentation, parent_presentation):
try:
folder_name = os.path.basename(os.path.dirname(video_path))
image_path = self.prepare_image_path(video_path)
if not os.path.exists(image_path):
if not self.extract_screenshots(video_path):
raise Exception("スクリーンショットの抽出に失敗しました。")
self.insert_image_to_ppt(sub_presentation, image_path, video_path)
self.add_notes_to_slide(sub_presentation, video_path)
self.insert_image_to_ppt(parent_presentation, image_path, video_path)
self.add_notes_to_slide(parent_presentation, video_path)
except Exception as e:
logging.exception(f"動画 {video_path} の処理中にエラーが発生しました: {e}")
raise
def process_videos(self):
logging.info(f"シングルスレッド処理: {self.single_thread_var.get()}")
processed_folders = 0
parent_presentation = Presentation()
parent_presentation.slide_width = Inches(13.33)
parent_presentation.slide_height = Inches(7.5)
skipped_videos_count = 0
video_details_cache = load_video_details_cache() # キャッシュの読み込み
try:
self.error_occurred = False
semaphore = threading.Semaphore(self.max_threads)
processed_videos_details_count = 0
for folder_path in self.all_folders:
self.update_status(current=processed_folders + 1, total=self.total_folders, folder_path=os.path.basename(folder_path))
sub_presentation = Presentation()
sub_presentation.slide_width = Inches(13.33)
sub_presentation.slide_height = Inches(7.5)
video_paths = self.get_all_video_files(folder_path)
if not video_paths:
continue
total_videos = len(video_paths)
processed_videos_in_folder = 0
if self.single_thread_var.get():
# シングルスレッド処理
for video_path in video_paths:
image_path = self.prepare_image_path(video_path)
if os.path.exists(image_path):
skipped_videos_count += 1
self.update_total_progress(current=self.processed_videos_count + skipped_videos_count)
self.selected_videos_label.config(text=f"総動画数: {self.processed_videos_count + skipped_videos_count}/{self.total_videos_count}")
logging.info(f"動画 {video_path} のアーカイブ画像が既に存在します。スキップします。")
else:
if not self.extract_screenshots(video_path):
logging.warning(f"動画 {video_path} のフレーム抽出に失敗しました。次の動画に移ります。")
else:
self.processed_videos_count += 1
processed_videos_in_folder += 1
self.update_total_progress(current=self.processed_videos_count + skipped_videos_count)
self.selected_videos_label.config(text=f"総動画数: {self.processed_videos_count + skipped_videos_count}/{self.total_videos_count}")
else:
# マルチスレッド処理
frame_extraction_threads = []
chunk_size = total_videos // self.max_threads + 1
for i in range(0, total_videos, chunk_size):
chunk_paths = video_paths[i:i+chunk_size]
thread = threading.Thread(target=self.process_frame_extraction_thread, args=(chunk_paths, semaphore, folder_path))
thread.start()
frame_extraction_threads.append(thread)
while any(thread.is_alive() for thread in frame_extraction_threads) or not self.progress_queue.empty():
try:
progress_type, video_path, folder_path, *args = self.progress_queue.get(block=False)
if progress_type == "extraction_success":
self.processed_videos_count += 1
processed_videos_in_folder += 1
self.update_total_progress(current=self.processed_videos_count + skipped_videos_count)
self.selected_videos_label.config(text=f"総動画数: {self.processed_videos_count + skipped_videos_count}/{self.total_videos_count}")
elif progress_type == "extraction_skipped":
skipped_videos_count += 1
self.update_total_progress(current=self.processed_videos_count + skipped_videos_count)
self.selected_videos_label.config(text=f"総動画数: {self.processed_videos_count + skipped_videos_count}/{self.total_videos_count}")
logging.info(f"動画 {video_path} のアーカイブ画像が既に存在します。スキップします。")
elif progress_type == "extraction_failed":
logging.warning(f"動画 {video_path} のフレーム抽出に失敗しました。次の動画に移ります。")
except queue.Empty:
pass
if self.master.winfo_exists():
self.master.update() # Update UI
for thread in frame_extraction_threads:
thread.join()
processed_videos_details = {}
if self.add_details_var.get():
for video_path in video_paths:
try:
video_name = os.path.basename(video_path)
if video_name in video_details_cache:
file_size, video_duration_min, video_duration_sec = video_details_cache[video_name]
else:
file_info = os.stat(video_path)
file_size = file_info.st_size / (1024 * 1024)
video_duration = get_video_duration(video_path)
video_duration_min = int(video_duration // 60)
video_duration_sec = int(video_duration % 60)
video_details_cache[video_name] = (file_size, video_duration_min, video_duration_sec)
processed_videos_details[(folder_path, video_path)] = (file_size, video_duration_min, video_duration_sec)
processed_videos_details_count += 1
self.update_detail_extraction_progress(current=processed_videos_details_count, total=self.total_videos_count)
logging.info(f"動画 {video_path} の詳細情報を取得しました。ファイルサイズ: {file_size:.2f} MB, 動画の長さ: {video_duration_min}分{video_duration_sec}秒")
except Exception as e:
logging.exception(f"動画 {video_path} の詳細情報の取得中にエラーが発生しました: {e}")
save_video_details_cache(video_details_cache) # フォルダ処理後にキャッシュを保存
for video_path in video_paths:
if self.grid_size_var.get() not in ["2x6", "3x9", "4x12", "5x15", "6x18", "7x21"]: # 縦長のグリッドサイズでない場合のみPPTファイルを作成
image_path = self.prepare_image_path(video_path)
self.insert_image_to_ppt(sub_presentation, image_path, video_path)
if self.add_details_var.get() and (folder_path, video_path) in processed_videos_details:
file_size, video_duration_min, video_duration_sec = processed_videos_details[(folder_path, video_path)]
self.add_notes_to_slide(sub_presentation, video_path, file_size, video_duration_min, video_duration_sec)
else:
self.add_notes_to_slide(sub_presentation, video_path)
self.insert_image_to_ppt(parent_presentation, image_path, video_path)
if self.add_details_var.get() and (folder_path, video_path) in processed_videos_details:
file_size, video_duration_min, video_duration_sec = processed_videos_details[(folder_path, video_path)]
self.add_notes_to_slide(parent_presentation, video_path, file_size, video_duration_min, video_duration_sec)
else:
self.add_notes_to_slide(parent_presentation, video_path)
if self.grid_size_var.get() not in ["2x6", "3x9", "4x12", "5x15", "6x18", "7x21"]: # 縦長のグリッドサイズでない場合のみPPTファイルを保存
sub_ppt_save_path = os.path.join(folder_path, f"{os.path.basename(folder_path)}.pptx")
if os.path.exists(sub_ppt_save_path):
os.remove(sub_ppt_save_path)
sub_presentation.save(sub_ppt_save_path)
processed_folders += 1
if self.grid_size_var.get() not in ["2x6", "3x9", "4x12", "5x15", "6x18", "7x21"]: # 縦長のグリッドサイズでない場合のみ親PPTファイルを保存
parent_ppt_save_path = os.path.join(self.video_folder_path, f"{os.path.basename(self.video_folder_path)}.pptx")
if os.path.exists(parent_ppt_save_path):
os.remove(parent_ppt_save_path)
parent_presentation.save(parent_ppt_save_path)
logging.info("全ての処理が完了しました。")
except Exception as e:
self.error_occurred = True
self.update_status("エラーが発生しました")
logging.exception("処理中にエラーが発生しました: %s", str(e))
messagebox.showerror("エラー", f"処理中にエラーが発生しました: {str(e)}")
finally:
# クリーンアップコード
for obj in gc.get_objects():
if isinstance(obj, VideoFileClip):
obj.close()
save_video_details_cache(video_details_cache) # 最終的にキャッシュを保存
if not self.error_occurred:
self.update_total_progress(current=self.total_videos_count)
self.update_detail_extraction_progress(current=self.total_videos_count, total=self.total_videos_count)
self.selected_videos_label.config(text=f"総動画数: {self.total_videos_count}/{self.total_videos_count}")
def update_detail_extraction_progress(self, current, total):
self.detail_extraction_progress["value"] = (current / total) * 100
self.detail_extraction_progress.update()
def update_folder_progress(self, current, total, folder_path):
progress = (current / total) * 100
self.folder_progress_bar["value"] = progress
self.status_label.config(text=f"処理中: {current}/{total}\n{folder_path}")
self.folder_progress_bar.update()
def get_all_folders(self, base_folder):
all_folders = []
for root, dirs, files in os.walk(base_folder):
if any(file.lower().endswith(('.mp4', '.avi', '.mov')) for file in files):
all_folders.append(root)
return all_folders
def count_total_videos(self, folder_path):
total_videos = 0
for root, _, files in os.walk(folder_path):
for file in files:
if any(file.lower().endswith(ext) for ext in {'.mp4', '.avi', '.mov'}):
total_videos += 1
return total_videos
def update_total_progress(self, current):
self.progress_bar["value"] = current
self.progress_bar.update()
def reset_button_states(self):
self.execute_button.config(relief=tk.RAISED)
self.select_folder_button.config(relief=tk.RAISED)
self.update_status("待機中")
def prepare_image_path(self, video_path):
return os.path.join(os.path.dirname(video_path), os.path.splitext(os.path.basename(video_path))[0] + "_archive.jpg")
def add_notes_to_slide(self, prs, video_path, file_size=None, video_duration_min=None, video_duration_sec=None):
slide = prs.slides[-1]
notes_slide = slide.notes_slide
notes_text_frame = notes_slide.notes_text_frame
notes_text_frame.text = ""
if self.add_details_var.get():
if file_size is not None and video_duration_min is not None and video_duration_sec is not None:
notes_text_frame.text = f"ファイルサイズ: {file_size:.2f} MB/動画の長さ: {video_duration_min}分{video_duration_sec}秒\nファイルパス: {video_path}"
else:
notes_text_frame.text = video_path
else:
notes_text_frame.text = video_path
def extract_screenshots(self, video_path):
grid_size = self.grid_size_var.get()
grid_width, grid_height = map(int, grid_size.split('x'))
if grid_size in ["2x6", "3x9", "4x12", "5x15", "6x18", "7x21"]: # 縦長のグリッドサイズが選択された場合
total_image_size = (1920, 3240)
else:
total_image_size = (1920, 1080)
archive_count = grid_width * grid_height
sub_image_size = (total_image_size[0] // grid_width, total_image_size[1] // grid_height)
try:
clip = VideoFileClip(str(video_path))
video_name = os.path.splitext(os.path.basename(video_path))[0]
output_folder = os.path.dirname(video_path)
duration = clip.duration
start_time = 20 # original 60
logging.info(f"画像抽出開始: {video_name}")
if duration <= start_time:
raise ValueError("動画の長さが不十分です。")
remaining_duration = duration - start_time
archive_interval = remaining_duration / archive_count
archive_images = []
for i in range(archive_count):
frame_time = start_time + i * archive_interval
frame = clip.get_frame(frame_time)
img = Image.fromarray(frame).resize(sub_image_size)
img_path = os.path.join(str(output_folder), f'{video_name}_archive_{i+1}.jpg')
img.save(img_path)
archive_images.append(img)
combined_image_path = os.path.join(str(output_folder), f'{video_name}_archive.jpg')
self.resize_and_combine_images(archive_images, combined_image_path, grid_width, grid_height)
for img_path in [os.path.join(str(output_folder), f'{video_name}_archive_{i+1}.jpg') for i in range(archive_count)]:
os.remove(img_path)
logging.info(f"画像抽出完了: {video_name}")
except Exception as e:
logging.exception(f"動画 {video_path} のスクリーンショット抽出中にエラーが発生しました: {e}")
return False
finally:
if 'clip' in locals():
clip.close()
return True
def resize_and_combine_images(self, images, output_path, grid_width, grid_height):
try:
grid_size = f"{grid_width}x{grid_height}"
if grid_size in ["2x6", "3x9", "4x12", "5x15", "6x18", "7x21"]: # 縦長のグリッドサイズが選択された場合
total_image_size = (1920, 3240)
else:
total_image_size = (1920, 1080)
sub_image_size = (total_image_size[0] // grid_width, total_image_size[1] // grid_height)
new_image = Image.new('RGB', total_image_size)
for i, img in enumerate(images):
x = (i % grid_width) * sub_image_size[0]
y = (i // grid_width) * sub_image_size[1]
resized_img = img.resize(sub_image_size)
new_image.paste(resized_img, (x, y))
new_image.save(output_path)
except Exception as e:
logging.exception(f"画像の結合中にエラーが発生しました: {e}")
def insert_image_to_ppt(self, prs, image_path, hyperlink):
try:
# 白紙のスライドレイアウトを使用 (通常はインデックス6)
blank_slide_layout = prs.slide_layouts[6]
slide = prs.slides.add_slide(blank_slide_layout)
with Image.open(image_path) as img:
original_width, original_height = img.size
slide_width = prs.slide_width
slide_height = prs.slide_height
# 画像サイズの調整
ratio = min(slide_width / original_width, slide_height / original_height)
image_width = int(original_width * ratio)
image_height = int(original_height * ratio)
# 画像を中央に配置
left = (slide_width - image_width) / 2
top = (slide_height - image_height) / 2
# 画像をスライドに挿入
pic = slide.shapes.add_picture(image_path, left, top, width=image_width, height=image_height)
# ハイパーリンクを設定 (変更部分)
click_action = pic.click_action
click_action.hyperlink.address = hyperlink.replace('/', '\\')
except Exception as e:
logging.exception(f"画像の挿入中にエラーが発生しました: {e}")
def process_frame_extraction_thread(self, video_paths, semaphore, folder_path, batch_size=100, interval=5):
processed_count = 0
for video_path in video_paths:
with semaphore:
image_path = self.prepare_image_path(video_path)
if os.path.exists(image_path):
self.progress_queue.put(("extraction_skipped", video_path, folder_path))
else:
if not self.extract_screenshots(video_path):
logging.warning(f"動画 {video_path} のフレーム抽出に失敗しました。次の動画に移ります。")
self.progress_queue.put(("extraction_failed", video_path, folder_path))
else:
self.progress_queue.put(("extraction_success", video_path, folder_path))
processed_count += 1
if processed_count % batch_size == 0:
time.sleep(interval) # batch_size個の処理ごとにinterva5秒のインターバルを設ける
time.sleep(1) # 1秒のタイムラグを追加
def count_videos_in_folder(self, folder_path):
video_count = 0
for root, dirs, files in os.walk(folder_path):
for file in files:
if any(file.lower().endswith(ext) for ext in {'.mp4', '.avi', '.mov'}):
video_count += 1
return video_count
def stop_processing(self):
self.stop_event.set()
root = tk.Tk()
root.title("Ver1.4")
app = Application(master=root)
app.pack()
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
text_handler = TextHandler(app.log_text)
text_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger = logging.getLogger()
logger.addHandler(text_handler)
root.mainloop()
処理中のステータスラベルの動画数がいまいち合わないです(-_-;)