import
base64
import
concurrent.futures
import
io
import
json
import
os
import
subprocess
import
threading
import
tkinter as tk
from
ctypes
import
windll
from
tkinter
import
filedialog, messagebox, Toplevel
from
PIL
import
Image, ImageTk
from
scenedetect
import
open_video, SceneManager
from
scenedetect.detectors
import
ContentDetector
from
scenedetect.video_splitter
import
split_video_ffmpeg
windll.shcore.SetProcessDpiAwareness(
2
)
font
=
'simhei.ttf'
config_file
=
'config.json'
languages
=
{
'zh'
: {
'title'
:
'分镜探测切割 1.4'
,
'about'
:
'关于'
,
'select_video_file'
:
'选择视频文件'
,
'select_video_folder'
:
'选择视频文件夹'
,
'threshold'
:
'画面变化阈值:'
,
'start'
:
'开始切割分镜'
,
'open_folder'
:
'打开输出文件夹'
,
'readme'
:
"2024年9月22日 by 吾爱破解:rootcup\n说明:画面变化阈值 数字越小越灵敏,切割的分镜头就越多。\n程序调用Py scenedetect,实现类似达芬奇Resolve和剪映的场景剪切探测/智能镜头分割。\n选择视频文件,是单文件操作。选择视频文件夹,是批量操作。"
,
'select_error'
:
'错误'
,
'select_error_message'
:
'请选择一个视频文件或文件夹.'
,
'threshold_error'
:
'错误'
,
'threshold_error_message'
:
'阈值必须是一个有效的数字.'
,
'completion_message'
:
'视频分割已完成!分镜视频位于 {} 文件夹内。\n'
,
'about_info'
:
'感谢使用本程序!\n'
},
'en'
: {
'title'
:
'Scene Detection and Splitting 1.3'
,
'about'
:
'About'
,
'select_video_file'
:
'Select Video File'
,
'select_video_folder'
:
'Select Video Folder'
,
'threshold'
:
'Scene Change Threshold:'
,
'start'
:
'Start Splitting Scenes'
,
'open_folder'
:
'Open Output Folder'
,
'readme'
:
'September 22, 2024 52pj: rootcup\n Description: The smaller and more sensitive the number of screen change threshold, the more the shot will be cut. The program calls Py scenedetect to implement scene clipping detection/smart shot segmentation similar to Da Vinci Resolve and clipping. \n Select video file, is a single file operation. Select the video folder in batches'
,
'select_error'
:
'Error'
,
'select_error_message'
:
'Please select a video file or folder.'
,
'threshold_error'
:
'Error'
,
'threshold_error_message'
:
'Threshold must be a valid number.'
,
'completion_message'
:
'Video splitting is complete! The split scenes are located in the {} folder.\n'
,
'about_info'
:
'Thank you for using this program!\n'
}
}
current_lang
=
'zh'
def
load_config():
if
os.path.exists(config_file):
with
open
(config_file,
'r'
, encoding
=
'utf-8'
) as
file
:
config
=
json.load(
file
)
return
config
return
{
'threshold'
:
27
,
'last_folder'
: '
', '
language
': '
zh'}
def
save_config(config):
with
open
(config_file,
'w'
, encoding
=
'utf-8'
) as
file
:
json.dump(config,
file
, ensure_ascii
=
False
, indent
=
4
)
config
=
load_config()
current_lang
=
config.get(
'language'
,
'zh'
)
windll.shcore.SetProcessDpiAwareness(
2
)
font
=
'simhei.ttf'
readme
=
languages[current_lang][
'readme'
]
jpg1_base64
=
'/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAAwADADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwBZGkMxCtjngBqX9+Sfm+vzDihQvnuScdcUqoBGXG4BhjHU9fwqTYSVXQlg+QvqckU1XLZ3TMtLMd6sw3D5sYP0ohC9d3Pcbc0D6ajjhSVNwwP0NIkjeeAHLjP+eKmydxJclf7uyqyqBMgVtwyO2KBLX+v+ADttmZl9frS+blWDDrjAHFBUNM+TgAkmnGNSynnDHAXGP8aB6DJJfMGCvToc9KI2Ck8kZGMjqKV1QqxQY2nHXrSRIHPzBueAR0pD0sOBRWDGRmI56U1DunBx1PTNSCNQASmBjJLH9O1RopSZdwxyOlMWhKozcSAE9Oo69qdIQGbcSACP5HpUbJIkjYXIbI/OhjMc/IQSc5UH6UE2uJMPkXBbG0H2ogIIKksM85Hakk85gNytgcUKrqmPLPzHr/SgroSOELgtg56Dd/gKjzm4GeMHGB7U/dKPlERx+J/WkSF/OB24AOetAlof/9k='
root
=
tk.Tk()
root.title(languages[current_lang][
'title'
])
root.geometry(
'550x600'
)
root.configure(bg
=
'#f0f0f0'
) # 设置背景色
top_frame
=
tk.Frame(root, bg
=
'#f0f0f0'
)
top_frame.pack(side
=
tk.TOP, fill
=
tk.X)
btn_about
=
tk.Button(top_frame, text
=
languages[current_lang][
'about'
], fg
=
'black'
, font
=
(font,
10
,
'bold'
),
relief
=
tk.FLAT, command
=
lambda
: show_about_window())
btn_about.pack(side
=
tk.RIGHT, padx
=
10
, pady
=
10
)
language_menu
=
tk.Menubutton(top_frame, text
=
"Language"
, fg
=
'black'
, font
=
(font,
10
,
'bold'
), relief
=
tk.FLAT)
language_menu.menu
=
tk.Menu(language_menu, tearoff
=
0
)
language_menu[
"menu"
]
=
language_menu.menu
language_menu.menu.add_command(label
=
"中文"
, command
=
lambda
: change_language(
'zh'
))
language_menu.menu.add_command(label
=
"English"
, command
=
lambda
: change_language(
'en'
))
language_menu.pack(side
=
tk.RIGHT, padx
=
10
, pady
=
10
)
def
update_language():
root.title(languages[current_lang][
'title'
])
btn_about.config(text
=
languages[current_lang][
'about'
])
btn_select_file.config(text
=
languages[current_lang][
'select_video_file'
])
btn_select_folder.config(text
=
languages[current_lang][
'select_video_folder'
])
label_threshold.config(text
=
languages[current_lang][
'threshold'
])
btn_start.config(text
=
languages[current_lang][
'start'
])
btn_open_folder.config(text
=
languages[current_lang][
'open_folder'
])
text_video_paths.delete(
1.0
, tk.END)
text_video_paths.insert(tk.END, languages[current_lang][
'readme'
],
"tag_1"
)
button0_frame
=
tk.Frame(root, bg
=
'#f0f0f0'
)
button0_frame.pack(pady
=
20
)
btn_select_file
=
tk.Button(button0_frame, text
=
languages[current_lang][
'select_video_file'
], bg
=
'#4CAF50'
, fg
=
'white'
,
font
=
(font,
12
,
'bold'
),
relief
=
tk.FLAT, command
=
lambda
: select_file())
btn_select_file.pack(pady
=
20
, side
=
tk.LEFT, padx
=
10
)
btn_select_folder
=
tk.Button(button0_frame, text
=
languages[current_lang][
'select_video_folder'
], bg
=
'#4CAF50'
,
fg
=
'white'
, font
=
(font,
12
,
'bold'
),
relief
=
tk.FLAT, command
=
lambda
: select_folder())
btn_select_folder.pack(pady
=
10
, side
=
tk.RIGHT, padx
=
10
)
label_threshold
=
tk.Label(root, text
=
languages[current_lang][
'threshold'
], bg
=
'#f0f0f0'
, font
=
(font,
10
))
label_threshold.pack()
entry_threshold
=
tk.Entry(root, width
=
17
)
entry_threshold.insert(
0
,
str
(config.get(
'threshold'
,
27
)))
entry_threshold.pack(pady
=
5
)
button_frame
=
tk.Frame(root, bg
=
'#f0f0f0'
)
button_frame.pack(pady
=
20
)
btn_start
=
tk.Button(button_frame, text
=
languages[current_lang][
'start'
], bg
=
'#2196F3'
, fg
=
'white'
,
font
=
(font,
12
,
'bold'
),
relief
=
tk.FLAT, command
=
lambda
: start_detection())
btn_start.pack(side
=
tk.LEFT, padx
=
10
)
btn_open_folder
=
tk.Button(button_frame, text
=
languages[current_lang][
'open_folder'
], bg
=
'#FFC107'
, fg
=
'white'
,
font
=
(font,
12
,
'bold'
),
relief
=
tk.FLAT, command
=
lambda
: open_output_folder())
btn_open_folder.pack(side
=
tk.LEFT, padx
=
10
)
text_video_paths
=
tk.Text(root, height
=
4000
, width
=
6000
, bg
=
'#f0f0f0'
, font
=
(font,
10
))
text_video_paths.pack(padx
=
5
, pady
=
10
)
text_video_paths.tag_config(
"tag_1"
, foreground
=
"green"
)
text_video_paths.tag_config(
"tag_2"
, foreground
=
"blue"
)
text_video_paths.insert(tk.END, readme,
"tag_1"
)
selected_folder
=
config.get(
'last_folder'
, '')
video_files
=
[]
def
select_file():
global
video_files
video_files
=
[filedialog.askopenfilename(filetypes
=
[(
"MP4文件"
,
"*.mp4;*.MP4"
)])]
if
video_files:
update_video_paths()
def
select_folder():
global
selected_folder, video_files
selected_folder
=
filedialog.askdirectory()
if
selected_folder:
config[
'last_folder'
]
=
selected_folder
save_config(config)
video_files
=
[os.path.join(selected_folder, f)
for
f
in
os.listdir(selected_folder)
if
f.lower().endswith(
'.mp4'
)]
update_video_paths()
def
update_video_paths():
text_video_paths.delete(
1.0
, tk.END)
for
video_file
in
video_files:
text_video_paths.insert(tk.END, video_file
+
'\n'
)
def
start_detection():
if
not
video_files:
messagebox.showerror(languages[current_lang][
'select_error'
], languages[current_lang][
'select_error_message'
])
return
try
:
threshold
=
float
(entry_threshold.get())
config[
'threshold'
]
=
threshold
save_config(config)
except
ValueError:
messagebox.showerror(languages[current_lang][
'threshold_error'
],
languages[current_lang][
'threshold_error_message'
])
return
detection_thread
=
threading.Thread(target
=
run_detection, args
=
(threshold,))
detection_thread.start()
def
run_detection(threshold):
with concurrent.futures.ThreadPoolExecutor(max_workers
=
4
) as executor:
futures
=
[executor.submit(split_video_into_scenes, video_path, threshold)
for
video_path
in
video_files]
for
future
in
concurrent.futures.as_completed(futures):
try
:
future.result()
except
Exception as e:
messagebox.showerror(languages[current_lang][
'select_error'
],
f
"{languages[current_lang]['select_error_message']} {str(e)}"
)
text_video_paths.insert(tk.END,
'已处理完成全部\n'
,
"tag_1"
)
def
split_video_into_scenes(video_path, threshold
=
27.0
):
base_filename
=
os.path.splitext(os.path.basename(video_path))[
0
]
save_path
=
f
'./分镜切割/{base_filename}/'
if
not
os.path.exists(save_path):
os.makedirs(save_path)
output_file_template
=
f
'{save_path}Scene-$SCENE_NUMBER.mp4'
video
=
open_video(video_path)
scene_manager
=
SceneManager()
scene_manager.add_detector(ContentDetector(threshold
=
threshold))
scene_manager.detect_scenes(video, show_progress
=
True
)
scene_list
=
scene_manager.get_scene_list()
split_video_ffmpeg(video_path, scene_list, output_file_template
=
output_file_template, show_progress
=
True
,
show_output
=
True
)
update_completion_message(save_path)
def
update_completion_message(save_path):
message
=
languages[current_lang][
'completion_message'
].
format
(save_path)
text_video_paths.insert(tk.END, message,
'tag_2'
)
def
open_output_folder():
if
video_files:
base_filename
=
os.path.splitext(os.path.basename(video_files[
0
]))[
0
]
save_path
=
f
'./分镜切割/{base_filename}/'
if
os.path.exists(save_path):
subprocess.Popen(f
'explorer "{os.path.abspath(save_path)}"'
)
def
show_about_window():
about_window
=
Toplevel(root)
about_window.title(languages[current_lang][
'about'
])
about_window.geometry(
"300x300"
)
info_label
=
tk.Label(about_window, text
=
languages[current_lang][
'about_info'
], font
=
(font,
12
))
info_label.pack(pady
=
10
)
img1_data
=
base64.b64decode(jpg1_base64)
img1
=
Image.
open
(io.BytesIO(img1_data))
img1_tk
=
ImageTk.PhotoImage(img1)
img1_label
=
tk.Label(about_window, image
=
img1_tk)
img1_label.image
=
img1_tk
img1_label.pack(pady
=
5
)
def
change_language(lang):
global
current_lang
current_lang
=
lang
config[
'language'
]
=
current_lang
save_config(config)
update_language()
update_language()
root.mainloop()