mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 08:48:13 +00:00
Initial commit of scheduler gui
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
/uploads
|
||||
*.pyc
|
||||
/server_state.json
|
||||
/.scheduler_prefs
|
||||
|
||||
367
scheduler_gui.py
Executable file
367
scheduler_gui.py
Executable file
@@ -0,0 +1,367 @@
|
||||
#!/usr/bin/env python3
|
||||
import copy
|
||||
import logging
|
||||
import os.path
|
||||
import pathlib
|
||||
import socket
|
||||
from tkinter import *
|
||||
from tkinter import filedialog, messagebox
|
||||
from tkinter.ttk import Frame, Label, Entry, Combobox
|
||||
from tkinter.simpledialog import askstring
|
||||
|
||||
import psutil
|
||||
import requests
|
||||
|
||||
from lib.job_server import post_job_to_server
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
prefs_name = '.scheduler_prefs'
|
||||
label_width = 7
|
||||
header_padding = 6
|
||||
server_setup_timeout = 5
|
||||
|
||||
|
||||
def request_data(server_ip, payload, server_port=8080, timeout=2):
|
||||
try:
|
||||
req = requests.get(f'http://{server_ip}:{server_port}/{payload}', timeout=timeout)
|
||||
if req.ok:
|
||||
return req.json()
|
||||
except Exception as e:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# CheckListBox source - https://stackoverflow.com/questions/50398649/python-tkinter-tk-support-checklist-box
|
||||
class ChecklistBox(Frame):
|
||||
def __init__(self, parent, choices, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.vars = []
|
||||
for choice in choices:
|
||||
var = StringVar(value="")
|
||||
self.vars.append(var)
|
||||
cb = Checkbutton(self, text=choice, onvalue=choice, offvalue="", anchor="w", width=20,
|
||||
relief="flat", highlightthickness=0, variable=var)
|
||||
cb.pack(side="top", fill="x", anchor="w")
|
||||
|
||||
def getCheckedItems(self):
|
||||
values = []
|
||||
for var in self.vars:
|
||||
value = var.get()
|
||||
if value:
|
||||
values.append(value)
|
||||
return values
|
||||
|
||||
def resetCheckedItems(self):
|
||||
values = []
|
||||
for var in self.vars:
|
||||
var.set(value='')
|
||||
return values
|
||||
|
||||
|
||||
class ScheduleJob(Frame):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.server_hostname = None
|
||||
self.chosen_file = None
|
||||
self.clients = []
|
||||
self.renderer_info = {}
|
||||
self.priority = IntVar(value=2)
|
||||
|
||||
self.master.title("Schedule Job")
|
||||
self.pack(fill=BOTH, expand=True)
|
||||
|
||||
self.server_button = Button(self, text="", width=6, command=self.request_new_hostname)
|
||||
self.server_button.pack(fill=X, padx=5, expand=False)
|
||||
|
||||
# project frame
|
||||
project_frame = Frame(self)
|
||||
project_frame.pack(fill=X)
|
||||
|
||||
Label(project_frame, text="Project Settings").pack(side=TOP, pady=header_padding)
|
||||
|
||||
project_label = Label(project_frame, text="Project", width=label_width)
|
||||
project_label.pack(side=LEFT, padx=5, pady=5)
|
||||
|
||||
self.project_button = Button(project_frame, text="no file selected", width=6, command=self.choose_file_button)
|
||||
self.project_button.pack(fill=X, padx=5, expand=True)
|
||||
|
||||
# client frame
|
||||
client_frame = Frame(self)
|
||||
client_frame.pack(fill=X)
|
||||
|
||||
Label(client_frame, text="Client", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
|
||||
self.client_combo = Combobox(client_frame, state="readonly")
|
||||
self.client_combo.pack(fill=X, padx=5, expand=True)
|
||||
|
||||
# renderer frame
|
||||
renderer_frame = Frame(self)
|
||||
renderer_frame.pack(fill=X)
|
||||
|
||||
Label(renderer_frame, text="Renderer", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
self.renderer_combo = Combobox(renderer_frame, state="readonly")
|
||||
self.renderer_combo.pack(fill=X, padx=5, expand=True)
|
||||
self.renderer_combo.bind('<<ComboboxSelected>>', self.refresh_renderer_settings)
|
||||
|
||||
# priority frame
|
||||
priority_frame = Frame(self)
|
||||
priority_frame.pack(fill=X)
|
||||
|
||||
Label(priority_frame, text="Priority", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
Radiobutton(priority_frame, text="1", value=1, variable=self.priority).pack(anchor=W, side=LEFT, padx=5)
|
||||
Radiobutton(priority_frame, text="2", value=2, variable=self.priority).pack(anchor=W, side=LEFT, padx=5)
|
||||
Radiobutton(priority_frame, text="3", value=3, variable=self.priority).pack(anchor=W, side=LEFT, padx=5)
|
||||
|
||||
# output frame
|
||||
output_frame = Frame(self)
|
||||
output_frame.pack(fill=X)
|
||||
|
||||
Label(output_frame, text="Output", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
|
||||
self.output_entry = Entry(output_frame)
|
||||
self.output_entry.pack(side=LEFT, padx=5, expand=True, fill=X)
|
||||
|
||||
self.output_format = Combobox(output_frame, state="readonly", values=['JPG', 'MOV', 'PNG'], width=9)
|
||||
self.output_format.pack(padx=5, pady=5)
|
||||
self.output_format['state'] = DISABLED
|
||||
|
||||
# Blender
|
||||
self.blender_frame = None
|
||||
self.blender_cameras_frame = None
|
||||
self.blender_engine = StringVar(value='CYCLES')
|
||||
self.blender_pack_textures = BooleanVar(value=False)
|
||||
self.blender_render_all_frames = BooleanVar(value=False)
|
||||
self.blender_multiple_cameras = BooleanVar(value=False)
|
||||
self.blender_cameras_list = None
|
||||
|
||||
# Submit Button
|
||||
self.submit_frame = None
|
||||
|
||||
if os.path.exists(prefs_name):
|
||||
with open(prefs_name, 'r') as file:
|
||||
hostname = file.read()
|
||||
server_data = request_data(hostname, 'status', timeout=server_setup_timeout)
|
||||
if server_data:
|
||||
self.server_hostname = hostname
|
||||
self.server_button.configure(text=hostname)
|
||||
if not self.server_hostname:
|
||||
self.request_new_hostname()
|
||||
self.fetch_server_data()
|
||||
|
||||
def fetch_server_data(self):
|
||||
self.clients = request_data(self.server_hostname, 'clients', timeout=3) or []
|
||||
self.renderer_info = request_data(self.server_hostname, 'renderer_info', timeout=3) or {}
|
||||
|
||||
# update clients
|
||||
self.client_combo['values'] = self.clients
|
||||
if self.clients:
|
||||
self.client_combo.current(0)
|
||||
|
||||
# update available renders
|
||||
available_renderers = [x for x in self.renderer_info.keys() if self.renderer_info[x].get('available', False)]
|
||||
self.renderer_combo['values'] = available_renderers
|
||||
if available_renderers:
|
||||
self.renderer_combo.current(0)
|
||||
|
||||
self.refresh_renderer_settings()
|
||||
|
||||
def request_new_hostname(self):
|
||||
hostname = None
|
||||
while not hostname:
|
||||
user_hostname_input = askstring("Server Name", "What is the server's hostname?")
|
||||
if not user_hostname_input: # user input is none if they press cancel
|
||||
return
|
||||
server_status = request_data(user_hostname_input, 'status', timeout=server_setup_timeout)
|
||||
if not server_status:
|
||||
messagebox.showerror("Cannot connect", f"Cannot connect to server \"{user_hostname_input}\"")
|
||||
else:
|
||||
hostname = user_hostname_input
|
||||
self.server_hostname = hostname
|
||||
self.server_button.configure(text=self.server_hostname)
|
||||
|
||||
# save to prefs
|
||||
with open(prefs_name, 'w') as file:
|
||||
file.write(hostname)
|
||||
|
||||
def choose_file_button(self):
|
||||
self.chosen_file = filedialog.askopenfilename()
|
||||
button_text = os.path.basename(self.chosen_file) if self.chosen_file else "no file selected"
|
||||
self.project_button.configure(text=button_text)
|
||||
|
||||
# Update the output label
|
||||
self.output_entry.delete(0, END)
|
||||
if self.chosen_file:
|
||||
# Generate a default output name
|
||||
output_name = os.path.basename(self.chosen_file).split('.')[0] + '-output'
|
||||
self.output_entry.insert(0, os.path.basename(output_name))
|
||||
# Try to determine file type
|
||||
extension = self.chosen_file.split('.')[-1] # not the best way to do this
|
||||
for renderer, renderer_info in self.renderer_info.items():
|
||||
supported = [x.lower().strip('.') for x in renderer_info.get('supported_extensions', [])]
|
||||
if extension.lower().strip('.') in supported:
|
||||
if renderer in self.renderer_combo['values']:
|
||||
self.renderer_combo.set(renderer)
|
||||
|
||||
self.refresh_renderer_settings()
|
||||
|
||||
def refresh_renderer_settings(self, event=None):
|
||||
renderer = self.renderer_combo.get()
|
||||
|
||||
# clear old settings
|
||||
if self.blender_frame:
|
||||
self.blender_frame.pack_forget()
|
||||
|
||||
if renderer == 'blender':
|
||||
self.draw_blender_settings()
|
||||
self.draw_submit_button()
|
||||
|
||||
if self.renderer_info.get(renderer, {}).get('supported_export_formats', None):
|
||||
formats = self.renderer_info[renderer]['supported_export_formats']
|
||||
formats.sort()
|
||||
self.output_format['values'] = formats
|
||||
self.output_format['state'] = NORMAL
|
||||
self.output_format.current(formats.index('JPEG'))
|
||||
else:
|
||||
self.output_format['values'] = []
|
||||
self.output_format['state'] = DISABLED
|
||||
|
||||
def draw_submit_button(self):
|
||||
if hasattr(self, 'submit_frame') and self.submit_frame:
|
||||
self.submit_frame.forget()
|
||||
self.submit_frame = Frame(self)
|
||||
self.submit_frame.pack(fill=BOTH, expand=True)
|
||||
Label(self.submit_frame, text="").pack(fill=BOTH, expand=True)
|
||||
submit_button = Button(self.submit_frame, text="Submit", command=self.submit_job)
|
||||
submit_button.pack(fill=Y, anchor="s", pady=header_padding)
|
||||
|
||||
def draw_blender_settings(self):
|
||||
|
||||
scene_data = None
|
||||
|
||||
# get file stats
|
||||
if self.chosen_file:
|
||||
from utilities.blender_worker import get_scene_info
|
||||
scene_data = get_scene_info(self.chosen_file)
|
||||
|
||||
# blender settings
|
||||
self.blender_frame = Frame(self)
|
||||
self.blender_frame.pack(fill=X)
|
||||
|
||||
Label(self.blender_frame, text="Blender Settings").pack(side=TOP, pady=header_padding)
|
||||
|
||||
blender_engine_frame = Frame(self.blender_frame)
|
||||
blender_engine_frame.pack(fill=X)
|
||||
|
||||
Label(blender_engine_frame, text="Engine", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
|
||||
Radiobutton(blender_engine_frame, text="Cycles", value="CYCLES", variable=self.blender_engine).pack(
|
||||
anchor=W, side=LEFT, padx=5)
|
||||
Radiobutton(blender_engine_frame, text="Eevee", value="BLENDER_EEVEE", variable=self.blender_engine).pack(
|
||||
anchor=W, side=LEFT, padx=5)
|
||||
|
||||
# options
|
||||
pack_frame = Frame(self.blender_frame)
|
||||
pack_frame.pack(fill=X)
|
||||
|
||||
Label(pack_frame, text="Options", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
|
||||
Checkbutton(pack_frame, text="Pack Textures", variable=self.blender_pack_textures, onvalue=True, offvalue=False
|
||||
).pack(anchor=W, side=LEFT, padx=5)
|
||||
Checkbutton(pack_frame, text="Render All Frames", variable=self.blender_render_all_frames, onvalue=True,
|
||||
offvalue=False).pack(anchor=W, side=LEFT, padx=5)
|
||||
|
||||
# multi cams
|
||||
def draw_scene_cams(event=None):
|
||||
if scene_data:
|
||||
show_cams_checkbutton['state'] = NORMAL
|
||||
if self.blender_multiple_cameras.get():
|
||||
self.blender_cameras_frame = Frame(self.blender_frame)
|
||||
self.blender_cameras_frame.pack(fill=X)
|
||||
|
||||
Label(self.blender_cameras_frame, text="Cameras", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
||||
|
||||
choices = [f"{x['name']} - {int(float(x['lens']))}mm" for x in scene_data['cameras']]
|
||||
self.blender_cameras_list = ChecklistBox(self.blender_cameras_frame, choices, relief="sunken")
|
||||
self.blender_cameras_list.pack(padx=5, fill=X)
|
||||
elif self.blender_cameras_frame:
|
||||
self.blender_cameras_frame.pack_forget()
|
||||
else:
|
||||
show_cams_checkbutton['state'] = DISABLED
|
||||
if self.blender_cameras_frame:
|
||||
self.blender_cameras_frame.pack_forget()
|
||||
|
||||
show_cams_checkbutton = Checkbutton(pack_frame, text='Multiple Cameras', offvalue=False, onvalue=True,
|
||||
variable=self.blender_multiple_cameras, command=draw_scene_cams)
|
||||
show_cams_checkbutton.pack(side=LEFT, padx=5)
|
||||
show_cams_checkbutton['state'] = NORMAL if scene_data and scene_data.get('cameras', None) else DISABLED
|
||||
|
||||
def submit_job(self):
|
||||
|
||||
client = self.client_combo.get()
|
||||
|
||||
renderer = self.renderer_combo.get()
|
||||
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
||||
'renderer': renderer,
|
||||
'client': client,
|
||||
'output_path': os.path.join(os.path.dirname(self.chosen_file), self.output_entry.get()),
|
||||
'args': {},
|
||||
'name': None}
|
||||
job_list = []
|
||||
|
||||
input_path = self.chosen_file
|
||||
|
||||
temp_files = []
|
||||
if renderer == 'blender':
|
||||
if self.blender_pack_textures.get():
|
||||
from utilities.blender_worker import pack_blender_files
|
||||
new_path = pack_blender_files(path=input_path)
|
||||
if new_path:
|
||||
logger.info(f'Packed Blender file successfully: {new_path}')
|
||||
input_path = new_path
|
||||
temp_files.append(new_path)
|
||||
else:
|
||||
err_msg = f'Failed to pack Blender file: {input_path}'
|
||||
messagebox.showinfo("Error", err_msg)
|
||||
return
|
||||
# add all Blender args
|
||||
job_json['args']['engine'] = self.blender_engine.get()
|
||||
job_json['args']['render_all_frames'] = self.blender_render_all_frames.get()
|
||||
job_json['args']['export_format'] = self.output_format.get()
|
||||
|
||||
# multiple camera rendering
|
||||
selected_cameras = self.blender_cameras_list.getCheckedItems() if self.blender_cameras_list else None
|
||||
for cam in selected_cameras:
|
||||
job_copy = copy.deepcopy(job_json)
|
||||
job_copy['args']['camera'] = cam.split('-')[0].strip()
|
||||
job_copy['name'] = pathlib.Path(input_path).stem.replace(' ', '_') + "-" + cam.replace(' ', '')
|
||||
job_list.append(job_copy)
|
||||
|
||||
# Submit to server
|
||||
job_list = job_list or [job_json]
|
||||
result = post_job_to_server(input_path=input_path, job_list=job_list, client=client)
|
||||
if result.ok:
|
||||
messagebox.showinfo("Success", "Job successfully submitted to server.")
|
||||
else:
|
||||
messagebox.showinfo("Error", result.text or "Unknown error")
|
||||
|
||||
# clean up
|
||||
for temp in temp_files:
|
||||
os.remove(temp)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
root = Tk()
|
||||
root.geometry("500x600+300+300")
|
||||
root.maxsize(width=600, height=800)
|
||||
root.minsize(width=500, height=600)
|
||||
app = ScheduleJob()
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user