Add config file, l10n, session resuming

This commit is contained in:
ABelliqueux 2024-02-18 13:12:09 +01:00
parent 4d24f104d9
commit a19f713d16
5 changed files with 156 additions and 74 deletions

25
config.toml Normal file
View File

@ -0,0 +1,25 @@
[DEFAULT]
file_extension = 'JPG'
trigger_mode = 'key'
projects_folder = ''
# project_letter = A
onion_skin_onstartup = false
onionskin_alpha_default = 0.4
onionskin_fx = false
fullscreen_bool = true
screen_w = 1920
screen_h = 1080
framerate = 16
vflip = true
hflip = false
export_options = 'scale=1920:-1,crop=1920:1080:0:102'
[CAMERA]
# Nikon D40x
capturemode = 3 # use IR remote
imagesize = 2 # use size S (1936x1296)
whitebalance = 1 # Natural light
capturetarget = 0 # Internal memory
nocfcardrelease = 0 # Allow capture without sd card
recordingmedia = 1 # Write to RAM
[CHECK]
acpower = 0 # we d'rather have this set to 0 which means we're running on AC

Binary file not shown.

Binary file not shown.

201
main_c.py
View File

@ -1,82 +1,110 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
import collections import collections
import gettext
# DLSR support
import gphoto2 as gp
from itertools import count
import locale import locale
import os import os
from PIL import Image, ImageTk, ImageFilter
import sys import sys
import threading import threading
import time import time
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog, messagebox
import tomllib
from PIL import Image, ImageTk, ImageFilter
# DLSR support
import gphoto2 as gp
# async FFMPEG exportation support # async FFMPEG exportation support
import asyncio import asyncio
from ffmpeg.asyncio import FFmpeg from ffmpeg.asyncio import FFmpeg
from itertools import count
from send2trash import send2trash from send2trash import send2trash
# TODO # TODO : Todo List
# #
# X wait event mode # X wait event mode
# X remove frames # X remove frames
# X keep images in memory (odict > list ?) # X keep images in memory (odict > list ?)
# o resize images upon shooting/ crop output video ? # / resize images upon shooting/crop output video ?
# o project workflow # X project workflow
# X use a different folder for each project (A, B, C, etc...) # X use a different folder for each project (A, B, C, etc...)
# X startup : check for existing folder with name A, B, C, etc. # X startup : check for existing folder with name A, B, C, etc.
# - startup : offer to continue previous project or new project # X startup : offer to continue previous project or new project
# - if continue, find last frame in folder X else, find next letter and create folder # X if continue, find last frame in folder X else, find next letter and create folder
# o notify export endingS # o notify export ending
# X colored onion skin frame (pillow filters) # X colored onion skin frame (pillow filters)
# V startup frame # X startup frame
# o open existing project screen # o webcam support (pygame, win and linux only)
# X Import config values from config file
# X Translation
# o Better settings names
running_from_folder = os.path.realpath(__file__) running_from_folder = os.path.realpath(__file__)
alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
project_settings = { # l10n
LOCALE = os.getenv('LANG', 'en_EN')
_ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext
# Config
# defaults
project_settings_defaults = {
'file_extension':'JPG', 'file_extension':'JPG',
'trigger_mode':'', 'trigger_mode': 'key',
'projects_folder': '', 'projects_folder': '',
# ~ 'project_letter': 'A' # ~ 'project_letter': 'A'
'onion_skin_onstartup' : False,
'onionskin_alpha_default' : 0.4,
'onionskin_fx' : False,
'fullscreen_bool' : True,
'screen_w' : 640,
'screen_h' : 480,
'framerate' : 16,
'vflip' : False,
'hflip' : False,
'export_options' : 'scale=1920:-1,crop=1920:1080:0:102',
} }
# Camera Settings (Nikon D40x)
# See `gphoto2 --list-config` to find your camera's values
camera_settings = camera_status = {}
# Load from file
config_locations = ["./", "~/.", "~/.config/"]
config_found_msg = _("No configuration file found, using defaults.")
project_settings = project_settings_defaults
for location in config_locations:
# Optional config files, ~ is expanded to $HOME on *nix, %USERPROFILE% on windows
if os.path.exists( os.path.expanduser(os.path.join(location, 'config.toml'))):
with open(os.path.expanduser(location + 'config.toml'), 'rb') as config_file:
project_settings = tomllib.load(config_file)
if 'DEFAULT' in project_settings:
project_settings = project_settings['DEFAULT']
if 'CAMERA' in project_settings:
camera_settings = project_settings['CAMERA']
if 'CHECK' in project_settings:
camera_settings = project_settings['CHECK']
config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location))
print(config_found_msg)
# TODO : Check resulting config has needed settings, fill missing ones
class KISStopmo(tk.Tk): class KISStopmo(tk.Tk):
def __init__(self, *args, **kargs): def __init__(self, *args, **kargs):
self.check_config()
print(project_settings)
# Default config # Default config
# TODO : Import value from config file # Set script settings according to config file
# Script settings self.onion_skin = project_settings['onion_skin_onstartup']
self.onion_skin = False self.onionskin_alpha = project_settings['onionskin_alpha_default']
self.onionskin_alpha = 0.4 self.onionskin_fx = project_settings['onionskin_fx']
self.onionskin_fx = False self.fullscreen_bool = project_settings['fullscreen_bool']
self.fullscreen_bool = True self.screen_w, self.screen_h = project_settings['screen_w'], project_settings['screen_h']
self.screen_w, self.screen_h = 640, 480 self.framerate = project_settings['framerate']
self.framerate = 16
self.photo = None
# ~ self.savepath = os.getcwd()
# ~ for setting in camera_settings:
# ~ print(setting)
# ~ self.photo = None
self.end_thread = False self.end_thread = False
# ~ self.project_settings = {'file_extension' : 'JPG'}
# ~ self.project_letter = "A"
# Camera Settings
self.camera_settings = {'capturemode': 3,
'imagesize': 2,
'whitebalance': 5,
'capturetarget': 0,
'nocfcardrelease': 0,
'recordingmedia' : 1
}
self.camera_status = { 'acpower': 0 # we d'rather have this set to 0 which means we're running on AC
}
# Window setup # Window setup
self.screen_w, self.screen_h = root.winfo_screenwidth(), root.winfo_screenheight() self.screen_w, self.screen_h = root.winfo_screenwidth(), root.winfo_screenheight()
root.wm_attributes("-fullscreen", self.fullscreen_bool) root.wm_attributes("-fullscreen", self.fullscreen_bool)
@ -87,8 +115,7 @@ class KISStopmo(tk.Tk):
self.label.pack() self.label.pack()
# Savepath setup # Savepath setup
self.projects_folder = tk.filedialog.askdirectory()
self.projects_folder = filedialog.askdirectory()
self.savepath = self.get_session_folder() self.savepath = self.get_session_folder()
if len(self.savepath): if len(self.savepath):
self.project_letter = self.savepath.split(os.sep)[-1] self.project_letter = self.savepath.split(os.sep)[-1]
@ -100,8 +127,7 @@ class KISStopmo(tk.Tk):
self.input_options = {"f": "image2", "r": str(self.framerate)} self.input_options = {"f": "image2", "r": str(self.framerate)}
# ~ self.output_filename = "{folder}{sep}{filename}.mp4".format(folder=self.projects_folder, sep=os.sep, filename=self.savepath.split(os.sep)[-1]) # ~ self.output_filename = "{folder}{sep}{filename}.mp4".format(folder=self.projects_folder, sep=os.sep, filename=self.savepath.split(os.sep)[-1])
self.output_filename = "{filename}.mp4".format(filename=self.project_letter) self.output_filename = "{filename}.mp4".format(filename=self.project_letter)
self.output_options = {"vf":"scale=1920:-1,crop=1920:1080:0:102"} self.output_options = self.parse_export_options(project_settings['export_options'], project_settings['vflip'], project_settings['hflip'] )
# Get frames list # Get frames list
self.img_list = {} self.img_list = {}
self.img_list = self.get_frames_list(self.savepath) self.img_list = self.get_frames_list(self.savepath)
@ -115,9 +141,9 @@ class KISStopmo(tk.Tk):
self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera)) self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera))
self.apply_camera_settings(self.camera, self.current_camera_config) self.apply_camera_settings(self.camera, self.current_camera_config)
if self.check_status(self.camera, self.current_camera_config) is False: if self.check_status(self.camera, self.current_camera_config) is False:
print("Warning: Some settings are not set to the recommended value!") print(_("Warning: Some settings are not set to the recommended value!"))
except: except:
self.splash_text = 'Camera not found or busy.' self.splash_text = _("Camera not found or busy.")
self.timeout = 3000 # milliseconds self.timeout = 3000 # milliseconds
@ -151,15 +177,32 @@ class KISStopmo(tk.Tk):
if project_settings['trigger_mode'] != 'event': if project_settings['trigger_mode'] != 'event':
root.bind("<j>", self.capture_image) root.bind("<j>", self.capture_image)
def check_config(self):
global project_settings
for setting in project_settings_defaults:
if setting not in project_settings:
project_settings[setting] = project_settings_defaults[setting]
def parse_export_options(self, options:str, vflip:bool=False, hflip:bool=False):
options = {"vf" : options}
if vflip:
options['vf'] += ',vflip'
if hflip:
options['vf'] += ',hflip'
return options
def generate_splashscreen(self): def generate_splashscreen(self):
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from io import BytesIO from io import BytesIO
img = Image.new('RGB', (self.screen_w, self.screen_h), (128,128,128)) img = Image.new('RGB', (self.screen_w, self.screen_h), (128,128,128))
d = ImageDraw.Draw(img) d = ImageDraw.Draw(img)
if self.splash_text is not None: if self.splash_text is not None:
fnt = ImageFont.truetype("FreeMono", 100) fnt = ImageFont.truetype("FreeMono", 50)
d.text((900, 500 ), self.splash_text, fill=(255, 0, 0), font=fnt) fnt_len = fnt.getlength(self.splash_text)
d.text((self.screen_w/2 - fnt_len/2, self.screen_h/2 ), self.splash_text, fill=(255, 0, 0), font=fnt)
# Save to file # Save to file
# img.save('test.png') # img.save('test.png')
# Use in-memory # Use in-memory
@ -212,13 +255,19 @@ class KISStopmo(tk.Tk):
if len(sessions_list): if len(sessions_list):
sessions_list.sort() sessions_list.sort()
last_letter = sessions_list[-1] last_letter = sessions_list[-1]
# By default, find next letter for a new session
next_letter = self.find_letter_after(last_letter) next_letter = self.find_letter_after(last_letter)
if next_letter is False: if next_letter is False:
return False return False
# A previous session folder was found; ask the user if they wish to resume session
resume_session = tk.messagebox.askyesno(_("Resume session?"), _("A previous session was found in {}, resume shooting ?").format(os.path.join(project_folder, last_letter)))
if resume_session:
next_letter = last_letter
else: else:
next_letter = 'A' next_letter = 'A'
if os.path.exists(os.path.join(project_folder, next_letter)) is False: if os.path.exists(os.path.join(project_folder, next_letter)) is False:
os.mkdir(os.path.join(project_folder, next_letter)) os.mkdir(os.path.join(project_folder, next_letter))
print(_("Using {} as session folder.").format(os.path.join(project_folder, next_letter)))
return os.path.join(project_folder, next_letter) return os.path.join(project_folder, next_letter)
return False return False
@ -234,9 +283,9 @@ class KISStopmo(tk.Tk):
cam_file = self.camera.file_get( cam_file = self.camera.file_get(
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL) event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
# ~ next_filename = prefix+next(filename)+ext # ~ next_filename = prefix+next(filename)+ext
next_filename = self.return_next_frame_number(self.get_last_frame(os.getcwd())) next_filename = self.return_next_frame_number(self.get_last_frame(self.savepath))
target_path = os.path.join(os.getcwd(), next_filename) target_path = os.path.join(os.getcwd(), next_filename)
print("Image is being saved to {}".format(target_path)) print(_("Image is being saved to {}").format(target_path))
cam_file.save(target_path) cam_file.save(target_path)
# ~ return 0 # ~ return 0
@ -252,6 +301,7 @@ class KISStopmo(tk.Tk):
existing_animation_files = self.img_list existing_animation_files = self.img_list
# ~ existing_animation_files = # ~ existing_animation_files =
file_list = os.listdir(folder) file_list = os.listdir(folder)
print(file_list)
for file in file_list: for file in file_list:
if (file.startswith(self.project_letter) and file.endswith(project_settings['file_extension'])): if (file.startswith(self.project_letter) and file.endswith(project_settings['file_extension'])):
if file not in existing_animation_files: if file not in existing_animation_files:
@ -291,7 +341,7 @@ class KISStopmo(tk.Tk):
# ~ os.rename(os.path.realpath(frame_list[i]), os.path.realpath("{}{}{}".format(project_settings['project_letter'], next(counter), project_settings['file_extension']))) # ~ os.rename(os.path.realpath(frame_list[i]), os.path.realpath("{}{}{}".format(project_settings['project_letter'], next(counter), project_settings['file_extension'])))
os.rename(os.path.realpath(i), os.path.realpath("{}{}{}".format(self.project_letter, next(counter), project_settings['file_extension']))) os.rename(os.path.realpath(i), os.path.realpath("{}{}{}".format(self.project_letter, next(counter), project_settings['file_extension'])))
else: else:
print(str(i) + " does not exist") print(_("{} does not exist").format(str(i)))
return self.get_frames_list(folder) return self.get_frames_list(folder)
@ -304,7 +354,7 @@ class KISStopmo(tk.Tk):
self.img_list[list(self.img_list.keys())[i]] = None self.img_list[list(self.img_list.keys())[i]] = None
# FIXME # FIXME: Does this still work ?
def remove_frame(self, event): def remove_frame(self, event):
if len(list(self.img_list.items())): if len(list(self.img_list.items())):
@ -316,7 +366,7 @@ class KISStopmo(tk.Tk):
return 0 return 0
# ~ print(self.img_list) # ~ print(self.img_list)
print("removing {}".format(frame_path)) print(_("Removing {}").format(frame_path))
# trash file # trash file
send2trash(frame_path) send2trash(frame_path)
# remove entry from dict # remove entry from dict
@ -337,15 +387,18 @@ class KISStopmo(tk.Tk):
self.update_image(None, self.img_index) self.update_image(None, self.img_index)
# ~ def open_jpg(self, filepath:str, w:int, h:int): # ~ def open_jpg(self, filepath:str, w:int, h:int):
def open_jpg(self, filetuple:tuple, w:int, h:int): def open_jpg(self, filetuple:tuple, w:int, h:int, vflip:bool=False, hflip:bool=False):
# If pic not cached # If pic not cached
if filetuple[-1] is None: if filetuple[-1] is None:
try: try:
# ~ image = Image.open(filepath) # ~ image = Image.open(filepath)
# ~ print( filetuple[0] + "is not in cache") # ~ print( filetuple[0] + "is not in cache")
image = Image.open(os.path.join(self.savepath, filetuple[0])) image = Image.open(os.path.join(self.savepath, filetuple[0]))
# TODO : Keep aspect ratio
image = image.resize((w, h)) image = image.resize((w, h))
if vflip:
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
if hflip:
image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
self.img_list[filetuple[0]] = image self.img_list[filetuple[0]] = image
except FileNotFoundError: except FileNotFoundError:
return False return False
@ -381,7 +434,7 @@ class KISStopmo(tk.Tk):
# ~ prev_image = prev_image.resize((self.screen_w, self.screen_h)) # ~ prev_image = prev_image.resize((self.screen_w, self.screen_h))
prev_image = list(self.img_list.items())[self.img_index-1] prev_image = list(self.img_list.items())[self.img_index-1]
if prev_image[-1] is None: if prev_image[-1] is None:
prev_image = self.open_jpg(prev_image, self.screen_w, self.screen_h) prev_image = self.open_jpg(prev_image, project_settings['screen_w'], project_settings['screen_h'], project_settings['vflip'], project_settings['hflip'])
else: else:
prev_image = prev_image[-1] prev_image = prev_image[-1]
if fx: if fx:
@ -402,7 +455,7 @@ class KISStopmo(tk.Tk):
if list(self.img_list.items())[index][0] == "{}.{:04d}.{}".format(self.project_letter, -1, project_settings['file_extension']): if list(self.img_list.items())[index][0] == "{}.{:04d}.{}".format(self.project_letter, -1, project_settings['file_extension']):
new_image = self.splashscreen new_image = self.splashscreen
else: else:
new_image = self.open_jpg(list(self.img_list.items())[index], self.screen_w, self.screen_h) new_image = self.open_jpg(list(self.img_list.items())[index], project_settings['screen_w'], project_settings['screen_h'], project_settings['vflip'], project_settings['hflip'])
else: else:
new_image = self.splashscreen new_image = self.splashscreen
# ~ return False # ~ return False
@ -412,7 +465,7 @@ class KISStopmo(tk.Tk):
photo = ImageTk.PhotoImage(new_image) photo = ImageTk.PhotoImage(new_image)
if self.onion_skin: if self.onion_skin:
if index: if index:
photo = self.apply_onionskin(new_image, self.onionskin_alpha, self.onionskin_fx) photo = self.apply_onionskin(new_image, project_settings['onionskin_alpha'], project_settings['onionskin_fx'])
# ~ print(photo) # ~ print(photo)
self.label.configure(image=photo) self.label.configure(image=photo)
self.label.image = photo self.label.image = photo
@ -489,14 +542,14 @@ class KISStopmo(tk.Tk):
def apply_camera_settings(self, camera, config): def apply_camera_settings(self, camera, config):
# iterate over the settings dictionary # iterate over the settings dictionary
for setting in self.camera_settings: for setting in camera_settings:
# find the capture mode config item # find the capture mode config item
# ~ cur_setting = gp.check_result(gp.gp_widget_get_child_by_name(config, setting)) # ~ cur_setting = gp.check_result(gp.gp_widget_get_child_by_name(config, setting))
# ~ # find corresponding choice # ~ # find corresponding choice
# ~ cur_setting_choice = gp.check_result(gp.gp_widget_get_choice(cur_setting, camera_settings[setting])) # ~ cur_setting_choice = gp.check_result(gp.gp_widget_get_choice(cur_setting, camera_settings[setting]))
# ~ # set config value # ~ # set config value
# ~ gp.check_result(gp.gp_widget_set_value(cur_setting, cur_setting_choice)) # ~ gp.check_result(gp.gp_widget_set_value(cur_setting, cur_setting_choice))
self.set_config_value(config, setting, self.camera_settings[setting]) self.set_config_value(config, setting, camera_settings[setting])
# validate config # validate config
gp.check_result(gp.gp_camera_set_config(self.camera, config)) gp.check_result(gp.gp_camera_set_config(self.camera, config))
@ -512,11 +565,11 @@ class KISStopmo(tk.Tk):
def check_status(self, camera, config): def check_status(self, camera, config):
for value in self.camera_status: for value in camera_status:
# ~ cur_check = gp.check_result(gp.gp_widget_get_child_by_name(config, value)) # ~ cur_check = gp.check_result(gp.gp_widget_get_child_by_name(config, value))
# ~ cur_check_choice = gp.check_result(gp.gp_widget_get_choice(cur_check, camera_status[value])) # ~ cur_check_choice = gp.check_result(gp.gp_widget_get_choice(cur_check, camera_status[value]))
# ~ cur_check_value = gp.check_result(gp.gp_widget_get_value(cur_check)) # ~ cur_check_value = gp.check_result(gp.gp_widget_get_value(cur_check))
cur_check_value, cur_check_choice = self.check_status_value(config, value, self.camera_status) cur_check_value, cur_check_choice = self.check_status_value(config, value, camera_status)
if cur_check_value == cur_check_choice: if cur_check_value == cur_check_choice:
return True return True
else: else:
@ -535,7 +588,7 @@ class KISStopmo(tk.Tk):
speed = 30 speed = 30
if current_lightmeter_value < 0: # or current_lightmeter_value > 7: if current_lightmeter_value < 0: # or current_lightmeter_value > 7:
# ~ while current_lightmeter_value < -7: # ~ while current_lightmeter_value < -7:
print("speed too high") print(_("speed too high"))
while current_lightmeter_value < -7: while current_lightmeter_value < -7:
self.set_config_value(config, 'shutterspeed2', speed) # 0 to 46 self.set_config_value(config, 'shutterspeed2', speed) # 0 to 46
gp.check_result(gp.gp_camera_set_config(camera, config)) gp.check_result(gp.gp_camera_set_config(camera, config))
@ -548,7 +601,7 @@ class KISStopmo(tk.Tk):
print(speed) print(speed)
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value)) print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
if current_lightmeter_value > 0: if current_lightmeter_value > 0:
print("speed too low") print(_("Speed too low."))
while current_lightmeter_value > 7: while current_lightmeter_value > 7:
self.set_config_value(config, 'shutterspeed2', speed) # 0 to 46 self.set_config_value(config, 'shutterspeed2', speed) # 0 to 46
gp.check_result(gp.gp_camera_set_config(camera, config)) gp.check_result(gp.gp_camera_set_config(camera, config))
@ -576,9 +629,9 @@ class KISStopmo(tk.Tk):
# ~ self.camera.trigger_capture() # ~ self.camera.trigger_capture()
new_frame = self.camera.file_get( new_frame = self.camera.file_get(
new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL) new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL)
print("saving " + new_frame_path.folder + new_frame_path.name ) print(_("Saving {}{}").format(new_frame_path.folder, new_frame_path.name))
else: else:
print("getting file {}".format(event_data.name)) print(_("Getting file {}").format(event_data.name))
new_frame = self.camera.file_get( new_frame = self.camera.file_get(
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL) event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
new_frame.save(target_path) new_frame.save(target_path)
@ -593,9 +646,8 @@ class KISStopmo(tk.Tk):
def wait_for_capture(self): def wait_for_capture(self):
if self.end_thread == True: if self.end_thread == True:
print("ending thread") print(_("Ending thread"))
return return
# ~ print("waiting for capture")
event_type, event_data = self.camera.wait_for_event(self.timeout) event_type, event_data = self.camera.wait_for_event(self.timeout)
if event_type == gp.GP_EVENT_FILE_ADDED: if event_type == gp.GP_EVENT_FILE_ADDED:
# ~ print("file added") # ~ print("file added")
@ -623,16 +675,17 @@ class KISStopmo(tk.Tk):
def trigger_export_animation(self, event): def trigger_export_animation(self, event):
output_folder = filedialog.askdirectory() output_folder = filedialog.askdirectory()
output_folder = "{folder}{sep}{filename}".format(folder=output_folder, sep=os.sep, filename=self.output_filename) output_folder = "{folder}{sep}{filename}".format(folder=output_folder, sep=os.sep, filename=self.output_filename)
print("exporting to " + output_folder) print(_("Exporting to {}").format(output_folder))
self.export_task = asyncio.run(self.export_animation()) self.export_task = asyncio.run(self.export_animation())
# check with self.export_task.done() == True ? https://stackoverflow.com/questions/69350645/proper-way-to-retrieve-the-result-of-tasks-in-asyncio # check with self.export_task.done() == True ? https://stackoverflow.com/questions/69350645/proper-way-to-retrieve-the-result-of-tasks-in-asyncio
def end_bg_loop(KISStopmo): def end_bg_loop(KISStopmo):
KISStopmo.camera.exit()
KISStopmo.end_thread = True KISStopmo.end_thread = True
root = tk.Tk() root = tk.Tk()
toot = KISStopmo(root) toot = KISStopmo(root)
# ~ root.protocol('WM_DELETE_WINDOW', end_bg_loop(toot)) root.protocol('WM_DELETE_WINDOW', end_bg_loop(toot))
root.mainloop() root.mainloop()

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
gphoto2
pillow
python-ffmpeg
Send2Trash