mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Add job polish (#63)
* Remove legacy client * Misc cleanup * Add message box after submission success / fail * Use a new is_localhost method to handle localhost not including '.local' * Code cleanup * Fix issue where engine browser would think we're downloading forever * Add message box after submission success / fail * Use a new is_localhost method to handle localhost not including '.local' * Code cleanup * Fix issue where engine browser would think we're downloading forever * Add pubsub messages to serverproxy_manager.py * Add resolution, fps and renderer versions to add_job.py * Add cameras to add_job.py * Add message box after submission success / fail * Use a new is_localhost method to handle localhost not including '.local' * Code cleanup * Fix issue where engine browser would think we're downloading forever * Add message box after submission success / fail * Code cleanup * Add cameras to add_job.py * Add dynamic engine options and output format * Move UI work out of BG threads and add engine presubmission tasks * Submit dynamic args when creating a new job * Hide groups and show messagebox after submission * Choose file when pressing New Job in main window now
This commit is contained in:
@@ -418,7 +418,6 @@ def status():
|
|||||||
|
|
||||||
@server.get('/api/renderer_info')
|
@server.get('/api/renderer_info')
|
||||||
def renderer_info():
|
def renderer_info():
|
||||||
return_simple = request.args.get('simple', False)
|
|
||||||
renderer_data = {}
|
renderer_data = {}
|
||||||
for engine in EngineManager.supported_engines():
|
for engine in EngineManager.supported_engines():
|
||||||
# Get all installed versions of engine
|
# Get all installed versions of engine
|
||||||
@@ -426,10 +425,9 @@ def renderer_info():
|
|||||||
if installed_versions:
|
if installed_versions:
|
||||||
install_path = installed_versions[0]['path']
|
install_path = installed_versions[0]['path']
|
||||||
renderer_data[engine.name()] = {'is_available': RenderQueue.is_available_for_job(engine.name()),
|
renderer_data[engine.name()] = {'is_available': RenderQueue.is_available_for_job(engine.name()),
|
||||||
'versions': installed_versions}
|
'versions': installed_versions,
|
||||||
if not return_simple:
|
'supported_extensions': engine.supported_extensions,
|
||||||
renderer_data[engine.name()]['supported_extensions'] = engine.supported_extensions
|
'supported_export_formats': engine(install_path).get_output_formats()}
|
||||||
renderer_data[engine.name()]['supported_export_formats'] = engine(install_path).get_output_formats()
|
|
||||||
return renderer_data
|
return renderer_data
|
||||||
|
|
||||||
|
|
||||||
@@ -542,7 +540,8 @@ def start_server():
|
|||||||
|
|
||||||
# Set up the RenderQueue object
|
# Set up the RenderQueue object
|
||||||
RenderQueue.load_state(database_directory=server.config['UPLOAD_FOLDER'])
|
RenderQueue.load_state(database_directory=server.config['UPLOAD_FOLDER'])
|
||||||
DistributedJobManager.start()
|
ServerProxyManager.subscribe_to_listener()
|
||||||
|
DistributedJobManager.subscribe_to_listener()
|
||||||
|
|
||||||
thread = threading.Thread(target=eval_loop, kwargs={'delay_sec': Config.queue_eval_seconds}, daemon=True)
|
thread = threading.Thread(target=eval_loop, kwargs={'delay_sec': Config.queue_eval_seconds}, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import time
|
|||||||
import requests
|
import requests
|
||||||
from requests_toolbelt.multipart import MultipartEncoder, MultipartEncoderMonitor
|
from requests_toolbelt.multipart import MultipartEncoder, MultipartEncoderMonitor
|
||||||
|
|
||||||
|
from src.utilities.misc_helper import is_localhost
|
||||||
from src.utilities.status_utils import RenderStatus
|
from src.utilities.status_utils import RenderStatus
|
||||||
|
|
||||||
status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', RenderStatus.COMPLETED: 'green',
|
status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', RenderStatus.COMPLETED: 'green',
|
||||||
@@ -145,7 +146,7 @@ class RenderServerProxy:
|
|||||||
def post_job_to_server(self, file_path, job_list, callback=None):
|
def post_job_to_server(self, file_path, job_list, callback=None):
|
||||||
|
|
||||||
# bypass uploading file if posting to localhost
|
# bypass uploading file if posting to localhost
|
||||||
if self.hostname == socket.gethostname():
|
if is_localhost(self.hostname):
|
||||||
jobs_with_path = [{**item, "local_path": file_path} for item in job_list]
|
jobs_with_path = [{**item, "local_path": file_path} for item in job_list]
|
||||||
return requests.post(f'http://{self.hostname}:{self.port}/api/add_job', data=json.dumps(jobs_with_path),
|
return requests.post(f'http://{self.hostname}:{self.port}/api/add_job', data=json.dumps(jobs_with_path),
|
||||||
headers={'Content-Type': 'application/json'})
|
headers={'Content-Type': 'application/json'})
|
||||||
@@ -181,8 +182,8 @@ class RenderServerProxy:
|
|||||||
|
|
||||||
# --- Renderer --- #
|
# --- Renderer --- #
|
||||||
|
|
||||||
def get_renderer_info(self, timeout=5, simple=False):
|
def get_renderer_info(self, timeout=5):
|
||||||
all_data = self.request_data(f'renderer_info?simple={simple}', timeout=timeout)
|
all_data = self.request_data(f'renderer_info', timeout=timeout)
|
||||||
return all_data
|
return all_data
|
||||||
|
|
||||||
def delete_engine(self, engine, version, system_cpu=None):
|
def delete_engine(self, engine, version, system_cpu=None):
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
from pubsub import pub
|
||||||
|
from zeroconf import ServiceStateChange
|
||||||
|
|
||||||
from src.api.server_proxy import RenderServerProxy
|
from src.api.server_proxy import RenderServerProxy
|
||||||
|
|
||||||
|
|
||||||
@@ -5,6 +8,21 @@ class ServerProxyManager:
|
|||||||
|
|
||||||
server_proxys = {}
|
server_proxys = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def subscribe_to_listener(cls):
|
||||||
|
"""
|
||||||
|
Subscribes the private class method '__local_job_status_changed' to the 'status_change' pubsub message.
|
||||||
|
This should be called once, typically during the initialization phase.
|
||||||
|
"""
|
||||||
|
pub.subscribe(cls.__zeroconf_state_change, 'zeroconf_state_change')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __zeroconf_state_change(cls, hostname, state_change, info):
|
||||||
|
if state_change == ServiceStateChange.Added or state_change == ServiceStateChange.Updated:
|
||||||
|
cls.get_proxy_for_hostname(hostname)
|
||||||
|
else:
|
||||||
|
cls.server_proxys.pop(hostname)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_proxy_for_hostname(cls, hostname):
|
def get_proxy_for_hostname(cls, hostname):
|
||||||
found_proxy = cls.server_proxys.get(hostname)
|
found_proxy = cls.server_proxys.get(hostname)
|
||||||
@@ -14,3 +32,4 @@ class ServerProxyManager:
|
|||||||
cls.server_proxys[hostname] = new_proxy
|
cls.server_proxys[hostname] = new_proxy
|
||||||
found_proxy = new_proxy
|
found_proxy = new_proxy
|
||||||
return found_proxy
|
return found_proxy
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class DistributedJobManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start(cls):
|
def subscribe_to_listener(cls):
|
||||||
"""
|
"""
|
||||||
Subscribes the private class method '__local_job_status_changed' to the 'status_change' pubsub message.
|
Subscribes the private class method '__local_job_status_changed' to the 'status_change' pubsub message.
|
||||||
This should be called once, typically during the initialization phase.
|
This should be called once, typically during the initialization phase.
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Blender(BaseRenderEngine):
|
|||||||
raise FileNotFoundError(f'Python script not found: {script_path}')
|
raise FileNotFoundError(f'Python script not found: {script_path}')
|
||||||
raise Exception("Uncaught exception")
|
raise Exception("Uncaught exception")
|
||||||
|
|
||||||
def get_scene_info(self, project_path, timeout=10):
|
def get_project_info(self, project_path, timeout=10):
|
||||||
scene_info = {}
|
scene_info = {}
|
||||||
try:
|
try:
|
||||||
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_file_info.py')
|
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_file_info.py')
|
||||||
@@ -138,6 +138,7 @@ class Blender(BaseRenderEngine):
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
def get_detected_gpus(self):
|
def get_detected_gpus(self):
|
||||||
|
# no longer works on 4.0
|
||||||
engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
|
engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
|
||||||
capture_output=True).stdout.decode('utf-8')
|
capture_output=True).stdout.decode('utf-8')
|
||||||
gpu_names = re.findall(r"DETECTED GPU: (.+)", engine_output)
|
gpu_names = re.findall(r"DETECTED GPU: (.+)", engine_output)
|
||||||
@@ -149,6 +150,17 @@ class Blender(BaseRenderEngine):
|
|||||||
render_engines = [x.strip() for x in engine_output.split('Blender Engine Listing:')[-1].strip().splitlines()]
|
render_engines = [x.strip() for x in engine_output.split('Blender Engine Listing:')[-1].strip().splitlines()]
|
||||||
return render_engines
|
return render_engines
|
||||||
|
|
||||||
|
# UI and setup
|
||||||
|
def get_options(self):
|
||||||
|
options = [
|
||||||
|
{'name': 'engine', 'options': self.supported_render_engines()},
|
||||||
|
]
|
||||||
|
return options
|
||||||
|
|
||||||
|
def perform_presubmission_tasks(self, project_path):
|
||||||
|
packed_path = self.pack_project_file(project_path, timeout=30)
|
||||||
|
return packed_path
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
x = Blender.get_detected_gpus()
|
x = Blender.get_detected_gpus()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
self.__frame_percent_complete = 0.0
|
self.__frame_percent_complete = 0.0
|
||||||
|
|
||||||
# Scene Info
|
# Scene Info
|
||||||
self.scene_info = Blender(engine_path).get_scene_info(input_path)
|
self.scene_info = Blender(engine_path).get_project_info(input_path)
|
||||||
self.start_frame = int(self.scene_info.get('start_frame', 1))
|
self.start_frame = int(self.scene_info.get('start_frame', 1))
|
||||||
self.end_frame = int(self.scene_info.get('end_frame', self.start_frame))
|
self.end_frame = int(self.scene_info.get('end_frame', self.start_frame))
|
||||||
self.project_length = (self.end_frame - self.start_frame) + 1
|
self.project_length = (self.end_frame - self.start_frame) + 1
|
||||||
@@ -140,7 +140,7 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
x = Blender.get_scene_info('/Users/brett/Desktop/TC Previz/nallie_farm.blend')
|
x = Blender.get_project_info('/Users/brett/Desktop/TC Previz/nallie_farm.blend')
|
||||||
pprint.pprint(x)
|
pprint.pprint(x)
|
||||||
|
|
||||||
# logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S', level=logging.DEBUG)
|
# logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S', level=logging.DEBUG)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
# Get all cameras
|
# Get all cameras
|
||||||
|
scene = bpy.data.scenes[0]
|
||||||
cameras = []
|
cameras = []
|
||||||
for cam_obj in bpy.data.cameras:
|
for cam_obj in bpy.data.cameras:
|
||||||
user_map = bpy.data.user_map(subset={cam_obj}, value_types={'OBJECT'})
|
user_map = bpy.data.user_map(subset={cam_obj}, value_types={'OBJECT'})
|
||||||
@@ -12,10 +13,10 @@ for cam_obj in bpy.data.cameras:
|
|||||||
'lens': cam_obj.lens,
|
'lens': cam_obj.lens,
|
||||||
'lens_unit': cam_obj.lens_unit,
|
'lens_unit': cam_obj.lens_unit,
|
||||||
'sensor_height': cam_obj.sensor_height,
|
'sensor_height': cam_obj.sensor_height,
|
||||||
'sensor_width': cam_obj.sensor_width}
|
'sensor_width': cam_obj.sensor_width,
|
||||||
|
'is_active': scene.camera.name_full == cam_obj.name_full}
|
||||||
cameras.append(cam)
|
cameras.append(cam)
|
||||||
|
|
||||||
scene = bpy.data.scenes[0]
|
|
||||||
data = {'cameras': cameras,
|
data = {'cameras': cameras,
|
||||||
'engine': scene.render.engine,
|
'engine': scene.render.engine,
|
||||||
'frame_start': scene.frame_start,
|
'frame_start': scene.frame_start,
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class BaseRenderEngine(object):
|
|||||||
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
||||||
return help_doc
|
return help_doc
|
||||||
|
|
||||||
|
def get_project_info(self, project_path, timeout=10):
|
||||||
|
raise NotImplementedError(f"get_project_info not implemented for {cls.__name__}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_output_formats(cls):
|
def get_output_formats(cls):
|
||||||
raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}")
|
raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}")
|
||||||
@@ -62,3 +65,7 @@ class BaseRenderEngine(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_arguments(cls):
|
def get_arguments(cls):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def perform_presubmission_tasks(self, project_path):
|
||||||
|
return project_path
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,8 @@ class EngineManager:
|
|||||||
for engine in cls.supported_engines():
|
for engine in cls.supported_engines():
|
||||||
if extension in engine.supported_extensions:
|
if extension in engine.supported_extensions:
|
||||||
return engine
|
return engine
|
||||||
|
undefined_renderer_support = [x for x in cls.supported_engines() if not x.supported_extensions]
|
||||||
|
return undefined_renderer_support[0]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from src.engines.core.base_engine import *
|
from src.engines.core.base_engine import *
|
||||||
@@ -29,6 +30,46 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
logger.error("Failed to get FFMPEG version: {}".format(e))
|
logger.error("Failed to get FFMPEG version: {}".format(e))
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
def get_project_info(self, project_path, timeout=10):
|
||||||
|
return self.get_video_info_ffprobe(project_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_video_info_ffprobe(video_path):
|
||||||
|
try:
|
||||||
|
# Run ffprobe and parse the output as JSON
|
||||||
|
cmd = [
|
||||||
|
'ffprobe', '-v', 'quiet', '-print_format', 'json',
|
||||||
|
'-show_streams', '-select_streams', 'v', video_path
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
video_info = json.loads(result.stdout)
|
||||||
|
|
||||||
|
# Extract the necessary information
|
||||||
|
video_stream = video_info['streams'][0]
|
||||||
|
frame_rate = eval(video_stream['r_frame_rate'])
|
||||||
|
duration = float(video_stream['duration'])
|
||||||
|
width = video_stream['width']
|
||||||
|
height = video_stream['height']
|
||||||
|
|
||||||
|
# Calculate total frames (end frame)
|
||||||
|
total_frames = int(duration * frame_rate)
|
||||||
|
end_frame = total_frames - 1
|
||||||
|
|
||||||
|
# The start frame is typically 0
|
||||||
|
start_frame = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'frame_start': start_frame,
|
||||||
|
'frame_end': end_frame,
|
||||||
|
'fps': frame_rate,
|
||||||
|
'resolution_x': width,
|
||||||
|
'resolution_y': height
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_encoders(self):
|
def get_encoders(self):
|
||||||
raw_stdout = subprocess.check_output([self.renderer_path(), '-encoders'], stderr=subprocess.DEVNULL,
|
raw_stdout = subprocess.check_output([self.renderer_path(), '-encoders'], stderr=subprocess.DEVNULL,
|
||||||
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
timeout=SUBPROCESS_TIMEOUT).decode('utf-8')
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import copy
|
||||||
import os.path
|
import os.path
|
||||||
|
import pathlib
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal, Qt
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QSpinBox, QComboBox,
|
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QSpinBox, QComboBox,
|
||||||
QGroupBox, QCheckBox, QProgressBar, QPlainTextEdit
|
QGroupBox, QCheckBox, QProgressBar, QPlainTextEdit, QDoubleSpinBox, QMessageBox, QListWidget, QListWidgetItem
|
||||||
)
|
)
|
||||||
|
from requests import Response
|
||||||
|
|
||||||
from src.api.server_proxy import RenderServerProxy
|
from src.api.server_proxy import RenderServerProxy
|
||||||
from src.engines.engine_manager import EngineManager
|
from src.engines.engine_manager import EngineManager
|
||||||
@@ -15,10 +19,20 @@ from src.utilities.zeroconf_server import ZeroconfServer
|
|||||||
|
|
||||||
|
|
||||||
class NewRenderJobForm(QWidget):
|
class NewRenderJobForm(QWidget):
|
||||||
def __init__(self):
|
def __init__(self, project_path=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self.project_path = project_path
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
self.current_engine_options = None
|
||||||
|
self.file_format_combo = None
|
||||||
|
self.renderer_options_layout = None
|
||||||
|
self.cameras_list = None
|
||||||
|
self.cameras_group = None
|
||||||
|
self.renderer_version_combo = None
|
||||||
|
self.worker_thread = None
|
||||||
|
self.msg_box = None
|
||||||
self.engine_help_viewer = None
|
self.engine_help_viewer = None
|
||||||
self.raw_args = None
|
self.raw_args = None
|
||||||
self.submit_progress_label = None
|
self.submit_progress_label = None
|
||||||
@@ -34,21 +48,21 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.priority_input = None
|
self.priority_input = None
|
||||||
self.end_frame_input = None
|
self.end_frame_input = None
|
||||||
self.start_frame_input = None
|
self.start_frame_input = None
|
||||||
self.output_path_browse_button = None
|
|
||||||
self.output_path_input = None
|
self.output_path_input = None
|
||||||
self.scene_file_input = None
|
self.scene_file_input = None
|
||||||
self.scene_file_browse_button = None
|
self.scene_file_browse_button = None
|
||||||
self.job_name_input = None
|
self.job_name_input = None
|
||||||
|
|
||||||
# Setup
|
|
||||||
self.setWindowTitle("New Job")
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
# Job / Server Data
|
# Job / Server Data
|
||||||
self.server_proxy = RenderServerProxy(socket.gethostname())
|
self.server_proxy = RenderServerProxy(socket.gethostname())
|
||||||
self.renderer_info = None
|
self.renderer_info = None
|
||||||
self.project_info = None
|
self.project_info = None
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
self.setWindowTitle("New Job")
|
||||||
|
self.setup_ui()
|
||||||
|
self.setup_project()
|
||||||
|
|
||||||
# get renderer info in bg thread
|
# get renderer info in bg thread
|
||||||
t = threading.Thread(target=self.update_renderer_info)
|
t = threading.Thread(target=self.update_renderer_info)
|
||||||
t.start()
|
t.start()
|
||||||
@@ -59,39 +73,12 @@ class NewRenderJobForm(QWidget):
|
|||||||
# Main Layout
|
# Main Layout
|
||||||
main_layout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
|
|
||||||
# Server Group
|
|
||||||
# Server List
|
|
||||||
server_group = QGroupBox("Server")
|
|
||||||
server_layout = QVBoxLayout(server_group)
|
|
||||||
server_list_layout = QHBoxLayout()
|
|
||||||
server_list_layout.setSpacing(0)
|
|
||||||
self.server_input = QComboBox()
|
|
||||||
server_list_layout.addWidget(QLabel("Hostname:"), 1)
|
|
||||||
server_list_layout.addWidget(self.server_input, 3)
|
|
||||||
server_layout.addLayout(server_list_layout)
|
|
||||||
main_layout.addWidget(server_group)
|
|
||||||
self.update_server_list()
|
|
||||||
# Priority
|
|
||||||
priority_layout = QHBoxLayout()
|
|
||||||
priority_layout.addWidget(QLabel("Priority:"), 1)
|
|
||||||
self.priority_input = QComboBox()
|
|
||||||
self.priority_input.addItems(["High", "Medium", "Low"])
|
|
||||||
self.priority_input.setCurrentIndex(1)
|
|
||||||
priority_layout.addWidget(self.priority_input, 3)
|
|
||||||
server_layout.addLayout(priority_layout)
|
|
||||||
# Splitjobs
|
|
||||||
self.enable_splitjobs = QCheckBox("Automatically split render across multiple servers")
|
|
||||||
self.enable_splitjobs.setEnabled(True)
|
|
||||||
server_layout.addWidget(self.enable_splitjobs)
|
|
||||||
self.splitjobs_same_os = QCheckBox("Only render on same OS")
|
|
||||||
self.splitjobs_same_os.setEnabled(True)
|
|
||||||
server_layout.addWidget(self.splitjobs_same_os)
|
|
||||||
|
|
||||||
# Scene File Group
|
# Scene File Group
|
||||||
scene_file_group = QGroupBox("Project")
|
scene_file_group = QGroupBox("Project")
|
||||||
scene_file_layout = QVBoxLayout(scene_file_group)
|
scene_file_layout = QVBoxLayout(scene_file_group)
|
||||||
scene_file_picker_layout = QHBoxLayout()
|
scene_file_picker_layout = QHBoxLayout()
|
||||||
self.scene_file_input = QLineEdit()
|
self.scene_file_input = QLineEdit()
|
||||||
|
self.scene_file_input.setText(self.project_path)
|
||||||
self.scene_file_browse_button = QPushButton("Browse...")
|
self.scene_file_browse_button = QPushButton("Browse...")
|
||||||
self.scene_file_browse_button.clicked.connect(self.browse_scene_file)
|
self.scene_file_browse_button.clicked.connect(self.browse_scene_file)
|
||||||
scene_file_picker_layout.addWidget(self.scene_file_input)
|
scene_file_picker_layout.addWidget(self.scene_file_input)
|
||||||
@@ -110,10 +97,51 @@ class NewRenderJobForm(QWidget):
|
|||||||
scene_file_layout.addLayout(progress_layout)
|
scene_file_layout.addLayout(progress_layout)
|
||||||
main_layout.addWidget(scene_file_group)
|
main_layout.addWidget(scene_file_group)
|
||||||
|
|
||||||
|
# Server Group
|
||||||
|
# Server List
|
||||||
|
self.server_group = QGroupBox("Server")
|
||||||
|
server_layout = QVBoxLayout(self.server_group)
|
||||||
|
server_list_layout = QHBoxLayout()
|
||||||
|
server_list_layout.setSpacing(0)
|
||||||
|
self.server_input = QComboBox()
|
||||||
|
server_list_layout.addWidget(QLabel("Hostname:"), 1)
|
||||||
|
server_list_layout.addWidget(self.server_input, 3)
|
||||||
|
server_layout.addLayout(server_list_layout)
|
||||||
|
main_layout.addWidget(self.server_group)
|
||||||
|
self.update_server_list()
|
||||||
|
# Priority
|
||||||
|
priority_layout = QHBoxLayout()
|
||||||
|
priority_layout.addWidget(QLabel("Priority:"), 1)
|
||||||
|
self.priority_input = QComboBox()
|
||||||
|
self.priority_input.addItems(["High", "Medium", "Low"])
|
||||||
|
self.priority_input.setCurrentIndex(1)
|
||||||
|
priority_layout.addWidget(self.priority_input, 3)
|
||||||
|
server_layout.addLayout(priority_layout)
|
||||||
|
# Splitjobs
|
||||||
|
self.enable_splitjobs = QCheckBox("Automatically split render across multiple servers")
|
||||||
|
self.enable_splitjobs.setEnabled(True)
|
||||||
|
server_layout.addWidget(self.enable_splitjobs)
|
||||||
|
self.splitjobs_same_os = QCheckBox("Only render on same OS")
|
||||||
|
self.splitjobs_same_os.setEnabled(True)
|
||||||
|
server_layout.addWidget(self.splitjobs_same_os)
|
||||||
|
|
||||||
# Output Settings Group
|
# Output Settings Group
|
||||||
output_settings_group = QGroupBox("Output Settings")
|
self.output_settings_group = QGroupBox("Output Settings")
|
||||||
output_settings_layout = QVBoxLayout(output_settings_group)
|
output_settings_layout = QVBoxLayout(self.output_settings_group)
|
||||||
frame_range_layout = QHBoxLayout(output_settings_group)
|
# output path
|
||||||
|
output_path_layout = QHBoxLayout()
|
||||||
|
output_path_layout.addWidget(QLabel("Render name:"))
|
||||||
|
self.output_path_input = QLineEdit()
|
||||||
|
output_path_layout.addWidget(self.output_path_input)
|
||||||
|
output_settings_layout.addLayout(output_path_layout)
|
||||||
|
# file format
|
||||||
|
file_format_layout = QHBoxLayout()
|
||||||
|
file_format_layout.addWidget(QLabel("Format:"))
|
||||||
|
self.file_format_combo = QComboBox()
|
||||||
|
file_format_layout.addWidget(self.file_format_combo)
|
||||||
|
output_settings_layout.addLayout(file_format_layout)
|
||||||
|
# frame range
|
||||||
|
frame_range_layout = QHBoxLayout(self.output_settings_group)
|
||||||
self.start_frame_input = QSpinBox()
|
self.start_frame_input = QSpinBox()
|
||||||
self.start_frame_input.setRange(1, 99999)
|
self.start_frame_input.setRange(1, 99999)
|
||||||
self.end_frame_input = QSpinBox()
|
self.end_frame_input = QSpinBox()
|
||||||
@@ -123,39 +151,70 @@ class NewRenderJobForm(QWidget):
|
|||||||
frame_range_layout.addWidget(QLabel("to"))
|
frame_range_layout.addWidget(QLabel("to"))
|
||||||
frame_range_layout.addWidget(self.end_frame_input)
|
frame_range_layout.addWidget(self.end_frame_input)
|
||||||
output_settings_layout.addLayout(frame_range_layout)
|
output_settings_layout.addLayout(frame_range_layout)
|
||||||
# output path
|
# resolution
|
||||||
output_path_layout = QHBoxLayout()
|
resolution_layout = QHBoxLayout(self.output_settings_group)
|
||||||
output_path_layout.addWidget(QLabel("Render name:"))
|
self.resolution_x_input = QSpinBox()
|
||||||
self.output_path_input = QLineEdit()
|
self.resolution_x_input.setRange(1, 9999) # Assuming max resolution width 9999
|
||||||
# self.output_path_browse_button = QPushButton("Browse...")
|
self.resolution_x_input.setValue(1920)
|
||||||
# self.output_path_browse_button.clicked.connect(self.browse_output_path)
|
self.resolution_y_input = QSpinBox()
|
||||||
output_path_layout.addWidget(self.output_path_input)
|
self.resolution_y_input.setRange(1, 9999) # Assuming max resolution height 9999
|
||||||
output_path_layout.addWidget(self.output_path_browse_button)
|
self.resolution_y_input.setValue(1080)
|
||||||
output_settings_layout.addLayout(output_path_layout)
|
self.frame_rate_input = QDoubleSpinBox()
|
||||||
main_layout.addWidget(output_settings_group)
|
self.frame_rate_input.setRange(1, 9999) # Assuming max resolution width 9999
|
||||||
|
self.frame_rate_input.setDecimals(3)
|
||||||
|
self.frame_rate_input.setValue(23.976)
|
||||||
|
resolution_layout.addWidget(QLabel("Resolution:"))
|
||||||
|
resolution_layout.addWidget(self.resolution_x_input)
|
||||||
|
resolution_layout.addWidget(QLabel("x"))
|
||||||
|
resolution_layout.addWidget(self.resolution_y_input)
|
||||||
|
resolution_layout.addWidget(QLabel("@"))
|
||||||
|
resolution_layout.addWidget(self.frame_rate_input)
|
||||||
|
resolution_layout.addWidget(QLabel("fps"))
|
||||||
|
output_settings_layout.addLayout(resolution_layout)
|
||||||
|
# add group to layout
|
||||||
|
main_layout.addWidget(self.output_settings_group)
|
||||||
|
|
||||||
# Renderer Group
|
# Renderer Group
|
||||||
renderer_group = QGroupBox("Renderer Settings")
|
self.renderer_group = QGroupBox("Renderer Settings")
|
||||||
renderer_layout = QVBoxLayout(renderer_group)
|
renderer_group_layout = QVBoxLayout(self.renderer_group)
|
||||||
|
renderer_layout = QHBoxLayout()
|
||||||
|
renderer_layout.addWidget(QLabel("Renderer:"))
|
||||||
self.renderer_type = QComboBox()
|
self.renderer_type = QComboBox()
|
||||||
|
self.renderer_type.currentIndexChanged.connect(self.renderer_changed)
|
||||||
renderer_layout.addWidget(self.renderer_type)
|
renderer_layout.addWidget(self.renderer_type)
|
||||||
|
# Version
|
||||||
|
renderer_layout.addWidget(QLabel("Version:"))
|
||||||
|
self.renderer_version_combo = QComboBox()
|
||||||
|
renderer_layout.addWidget(self.renderer_version_combo)
|
||||||
|
renderer_group_layout.addLayout(renderer_layout)
|
||||||
|
# dynamic options
|
||||||
|
self.renderer_options_layout = QVBoxLayout()
|
||||||
|
renderer_group_layout.addLayout(self.renderer_options_layout)
|
||||||
# Raw Args
|
# Raw Args
|
||||||
raw_args_layout = QHBoxLayout(renderer_group)
|
raw_args_layout = QHBoxLayout(self.renderer_group)
|
||||||
raw_args_layout.addWidget(QLabel("Raw Args:"))
|
raw_args_layout.addWidget(QLabel("Raw Args:"))
|
||||||
self.raw_args = QLineEdit()
|
self.raw_args = QLineEdit()
|
||||||
raw_args_layout.addWidget(self.raw_args)
|
raw_args_layout.addWidget(self.raw_args)
|
||||||
args_help_button = QPushButton("?")
|
args_help_button = QPushButton("?")
|
||||||
args_help_button.clicked.connect(self.args_help_button_clicked)
|
args_help_button.clicked.connect(self.args_help_button_clicked)
|
||||||
raw_args_layout.addWidget(args_help_button)
|
raw_args_layout.addWidget(args_help_button)
|
||||||
renderer_layout.addLayout(raw_args_layout)
|
renderer_group_layout.addLayout(raw_args_layout)
|
||||||
main_layout.addWidget(renderer_group)
|
main_layout.addWidget(self.renderer_group)
|
||||||
|
|
||||||
|
# Cameras Group
|
||||||
|
self.cameras_group = QGroupBox("Cameras")
|
||||||
|
cameras_layout = QVBoxLayout(self.cameras_group)
|
||||||
|
self.cameras_list = QListWidget()
|
||||||
|
self.cameras_group.setHidden(True)
|
||||||
|
cameras_layout.addWidget(self.cameras_list)
|
||||||
|
main_layout.addWidget(self.cameras_group)
|
||||||
|
|
||||||
# Notes Group
|
# Notes Group
|
||||||
notes_group = QGroupBox("Additional Notes")
|
self.notes_group = QGroupBox("Additional Notes")
|
||||||
notes_layout = QVBoxLayout(notes_group)
|
notes_layout = QVBoxLayout(self.notes_group)
|
||||||
self.notes_input = QPlainTextEdit()
|
self.notes_input = QPlainTextEdit()
|
||||||
notes_layout.addWidget(self.notes_input)
|
notes_layout.addWidget(self.notes_input)
|
||||||
main_layout.addWidget(notes_group)
|
main_layout.addWidget(self.notes_group)
|
||||||
|
|
||||||
# Submit Button
|
# Submit Button
|
||||||
self.submit_button = QPushButton("Submit Job")
|
self.submit_button = QPushButton("Submit Job")
|
||||||
@@ -177,6 +236,17 @@ class NewRenderJobForm(QWidget):
|
|||||||
def update_renderer_info(self):
|
def update_renderer_info(self):
|
||||||
self.renderer_info = self.server_proxy.get_renderer_info()
|
self.renderer_info = self.server_proxy.get_renderer_info()
|
||||||
self.renderer_type.addItems(self.renderer_info.keys())
|
self.renderer_type.addItems(self.renderer_info.keys())
|
||||||
|
self.renderer_changed()
|
||||||
|
|
||||||
|
def renderer_changed(self):
|
||||||
|
# load the version numbers
|
||||||
|
current_renderer = self.renderer_type.currentText().lower() or self.renderer_type.itemText(0)
|
||||||
|
self.renderer_version_combo.clear()
|
||||||
|
self.file_format_combo.clear()
|
||||||
|
if current_renderer:
|
||||||
|
renderer_vers = [version_info['version'] for version_info in self.renderer_info[current_renderer]['versions']]
|
||||||
|
self.renderer_version_combo.addItems(renderer_vers)
|
||||||
|
self.file_format_combo.addItems(self.renderer_info[current_renderer]['supported_export_formats'])
|
||||||
|
|
||||||
def update_server_list(self):
|
def update_server_list(self):
|
||||||
clients = ZeroconfServer.found_hostnames()
|
clients = ZeroconfServer.found_hostnames()
|
||||||
@@ -184,34 +254,26 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.server_input.addItems(clients)
|
self.server_input.addItems(clients)
|
||||||
|
|
||||||
def browse_scene_file(self):
|
def browse_scene_file(self):
|
||||||
|
|
||||||
def get_project_info():
|
|
||||||
self.process_progress_bar.setHidden(False)
|
|
||||||
self.process_label.setHidden(False)
|
|
||||||
self.toggle_renderer_enablement(False)
|
|
||||||
output_name, _ = os.path.splitext(os.path.basename(file_name))
|
|
||||||
self.output_path_input.setText(output_name)
|
|
||||||
|
|
||||||
engine = EngineManager.engine_for_project_path(file_name)
|
|
||||||
self.project_info = engine().get_scene_info(file_name)
|
|
||||||
|
|
||||||
index = self.renderer_type.findText(engine.name().lower())
|
|
||||||
if index >= 0:
|
|
||||||
self.renderer_type.setCurrentIndex(index)
|
|
||||||
|
|
||||||
self.update_project_ui()
|
|
||||||
|
|
||||||
self.process_progress_bar.setHidden(True)
|
|
||||||
self.process_label.setHidden(True)
|
|
||||||
self.toggle_renderer_enablement(True)
|
|
||||||
|
|
||||||
file_name, _ = QFileDialog.getOpenFileName(self, "Select Scene File")
|
file_name, _ = QFileDialog.getOpenFileName(self, "Select Scene File")
|
||||||
if file_name:
|
if file_name:
|
||||||
self.scene_file_input.setText(file_name)
|
self.scene_file_input.setText(file_name)
|
||||||
# analyze the file
|
self.setup_project()
|
||||||
update_thread = threading.Thread(target=get_project_info)
|
|
||||||
update_thread.start()
|
|
||||||
|
|
||||||
|
def setup_project(self):
|
||||||
|
# UI stuff on main thread
|
||||||
|
self.process_progress_bar.setHidden(False)
|
||||||
|
self.process_label.setHidden(False)
|
||||||
|
self.toggle_renderer_enablement(False)
|
||||||
|
|
||||||
|
output_name, _ = os.path.splitext(os.path.basename(self.scene_file_input.text()))
|
||||||
|
output_name = output_name.replace(' ', '_')
|
||||||
|
self.output_path_input.setText(output_name)
|
||||||
|
file_name = self.scene_file_input.text()
|
||||||
|
|
||||||
|
# setup bg worker
|
||||||
|
self.worker_thread = GetProjectInfoWorker(window=self, project_path=file_name)
|
||||||
|
self.worker_thread.message_signal.connect(self.post_get_project_info_update)
|
||||||
|
self.worker_thread.start()
|
||||||
|
|
||||||
def browse_output_path(self):
|
def browse_output_path(self):
|
||||||
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
|
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
|
||||||
@@ -226,66 +288,233 @@ class NewRenderJobForm(QWidget):
|
|||||||
|
|
||||||
# -------- Update --------
|
# -------- Update --------
|
||||||
|
|
||||||
def update_project_ui(self):
|
def post_get_project_info_update(self):
|
||||||
|
"""Called by the GetProjectInfoWorker - Do not call directly."""
|
||||||
|
try:
|
||||||
|
# Set the best renderer we can find
|
||||||
|
input_path = self.scene_file_input.text()
|
||||||
|
engine = EngineManager.engine_for_project_path(input_path)
|
||||||
|
index = self.renderer_type.findText(engine.name().lower())
|
||||||
|
if index >= 0:
|
||||||
|
self.renderer_type.setCurrentIndex(index)
|
||||||
|
|
||||||
|
self.output_path_input.setText(os.path.basename(input_path))
|
||||||
|
|
||||||
|
# cleanup progress UI
|
||||||
|
self.process_progress_bar.setHidden(True)
|
||||||
|
self.process_label.setHidden(True)
|
||||||
|
self.toggle_renderer_enablement(True)
|
||||||
|
|
||||||
|
# Load scene data
|
||||||
self.start_frame_input.setValue(self.project_info.get('frame_start'))
|
self.start_frame_input.setValue(self.project_info.get('frame_start'))
|
||||||
self.end_frame_input.setValue(self.project_info.get('frame_end'))
|
self.end_frame_input.setValue(self.project_info.get('frame_end'))
|
||||||
|
self.resolution_x_input.setValue(self.project_info.get('resolution_x'))
|
||||||
|
self.resolution_y_input.setValue(self.project_info.get('resolution_y'))
|
||||||
|
self.frame_rate_input.setValue(self.project_info.get('fps'))
|
||||||
|
|
||||||
|
# Cameras
|
||||||
|
self.cameras_list.clear()
|
||||||
|
if self.project_info.get('cameras'):
|
||||||
|
self.cameras_group.setHidden(False)
|
||||||
|
found_active = False
|
||||||
|
for camera in self.project_info['cameras']:
|
||||||
|
# create the list items and make them checkable
|
||||||
|
item = QListWidgetItem(f"{camera['name']} - {camera['lens']}mm")
|
||||||
|
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
is_checked = camera['is_active'] or len(self.project_info['cameras']) == 1
|
||||||
|
found_active = found_active or is_checked
|
||||||
|
item.setCheckState(Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked)
|
||||||
|
self.cameras_list.addItem(item)
|
||||||
|
if not found_active:
|
||||||
|
self.cameras_list.item(0).setCheckState(Qt.CheckState.Checked)
|
||||||
|
else:
|
||||||
|
self.cameras_group.setHidden(True)
|
||||||
|
|
||||||
|
# Dynamic Engine Options
|
||||||
|
engine_name = self.renderer_type.currentText()
|
||||||
|
engine = EngineManager.engine_with_name(engine_name)
|
||||||
|
# clear old options
|
||||||
|
clear_layout(self.renderer_options_layout)
|
||||||
|
# dynamically populate option list
|
||||||
|
self.current_engine_options = engine().get_options()
|
||||||
|
for option in self.current_engine_options:
|
||||||
|
h_layout = QHBoxLayout()
|
||||||
|
label = QLabel(option['name'].capitalize() + ':')
|
||||||
|
h_layout.addWidget(label)
|
||||||
|
if option.get('options'):
|
||||||
|
combo_box = QComboBox()
|
||||||
|
for opt in option['options']:
|
||||||
|
combo_box.addItem(opt)
|
||||||
|
h_layout.addWidget(combo_box)
|
||||||
|
else:
|
||||||
|
text_box = QLineEdit()
|
||||||
|
h_layout.addWidget(text_box)
|
||||||
|
self.renderer_options_layout.addLayout(h_layout)
|
||||||
|
except AttributeError as e:
|
||||||
|
pass
|
||||||
|
|
||||||
def toggle_renderer_enablement(self, enabled=False):
|
def toggle_renderer_enablement(self, enabled=False):
|
||||||
self.start_frame_input.setEnabled(enabled)
|
"""Toggle on/off all the render settings"""
|
||||||
self.end_frame_input.setEnabled(enabled)
|
self.server_group.setHidden(not enabled)
|
||||||
self.notes_input.setEnabled(enabled)
|
self.output_settings_group.setHidden(not enabled)
|
||||||
self.output_path_input.setEnabled(enabled)
|
self.renderer_group.setHidden(not enabled)
|
||||||
|
self.notes_group.setHidden(not enabled)
|
||||||
|
if not enabled:
|
||||||
|
self.cameras_group.setHidden(True)
|
||||||
self.submit_button.setEnabled(enabled)
|
self.submit_button.setEnabled(enabled)
|
||||||
|
|
||||||
|
def after_job_submission(self, result):
|
||||||
|
|
||||||
|
# UI cleanup
|
||||||
|
self.submit_progress.setMaximum(0)
|
||||||
|
self.submit_button.setHidden(False)
|
||||||
|
self.submit_progress.setHidden(True)
|
||||||
|
self.submit_progress_label.setHidden(True)
|
||||||
|
self.process_progress_bar.setHidden(True)
|
||||||
|
self.process_label.setHidden(True)
|
||||||
|
self.toggle_renderer_enablement(True)
|
||||||
|
|
||||||
|
self.msg_box = QMessageBox()
|
||||||
|
if result.ok:
|
||||||
|
self.msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
self.msg_box.setIcon(QMessageBox.Icon.Information)
|
||||||
|
self.msg_box.setText("Job successfully submitted to server. Submit another?")
|
||||||
|
self.msg_box.setWindowTitle("Success")
|
||||||
|
x = self.msg_box.exec()
|
||||||
|
if x == QMessageBox.StandardButton.No:
|
||||||
|
self.close()
|
||||||
|
else:
|
||||||
|
self.msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
self.msg_box.setIcon(QMessageBox.Icon.Critical)
|
||||||
|
self.msg_box.setText(result.text or "Unknown error")
|
||||||
|
self.msg_box.setWindowTitle("Error")
|
||||||
|
self.msg_box.exec()
|
||||||
|
|
||||||
# -------- Submit Job Calls --------
|
# -------- Submit Job Calls --------
|
||||||
|
|
||||||
def submit_job(self):
|
def submit_job(self):
|
||||||
def submit_job_worker():
|
|
||||||
def create_callback(encoder):
|
|
||||||
encoder_len = encoder.len
|
|
||||||
def callback(monitor):
|
|
||||||
percent = f"{monitor.bytes_read / encoder_len * 100:.0f}"
|
|
||||||
self.submit_progress_label.setText(f"Transferring to {hostname} - {percent}%")
|
|
||||||
self.submit_progress.setMaximum(100)
|
|
||||||
self.submit_progress.setValue(int(percent))
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
|
# Pre-worker UI
|
||||||
self.submit_progress.setHidden(False)
|
self.submit_progress.setHidden(False)
|
||||||
self.submit_progress_label.setHidden(False)
|
self.submit_progress_label.setHidden(False)
|
||||||
self.submit_button.setHidden(True)
|
self.submit_button.setHidden(True)
|
||||||
|
self.submit_progress.setMaximum(0)
|
||||||
|
|
||||||
hostname = self.server_input.currentText()
|
# submit job in background thread
|
||||||
|
self.worker_thread = SubmitWorker(window=self)
|
||||||
|
self.worker_thread.message_signal.connect(self.after_job_submission)
|
||||||
|
self.worker_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
class SubmitWorker(QThread):
|
||||||
|
"""Worker class called to submit all the jobs to the server and update the UI accordingly"""
|
||||||
|
|
||||||
|
message_signal = pyqtSignal(Response)
|
||||||
|
|
||||||
|
def __init__(self, window):
|
||||||
|
super().__init__()
|
||||||
|
self.window = window
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
def create_callback(encoder):
|
||||||
|
encoder_len = encoder.len
|
||||||
|
|
||||||
|
def callback(monitor):
|
||||||
|
percent = f"{monitor.bytes_read / encoder_len * 100:.0f}"
|
||||||
|
self.window.submit_progress_label.setText(f"Transferring to {hostname} - {percent}%")
|
||||||
|
self.window.submit_progress.setMaximum(100)
|
||||||
|
self.window.submit_progress.setValue(int(percent))
|
||||||
|
|
||||||
|
return callback
|
||||||
|
|
||||||
|
hostname = self.window.server_input.currentText()
|
||||||
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
||||||
'renderer': self.renderer_type.currentText().lower(),
|
'renderer': self.window.renderer_type.currentText().lower(),
|
||||||
# 'input_path': self.scene_file_input.text(),
|
'renderer_version': self.window.renderer_version_combo.currentText(),
|
||||||
# 'output_path': os.path.join(os.path.dirname(self.chosen_file), self.output_entry.get()),
|
'args': {'raw': self.window.raw_args.text()},
|
||||||
'args': {'raw': self.raw_args.text()},
|
'output_path': self.window.output_path_input.text(),
|
||||||
'output_path': self.output_path_input.text(),
|
'start_frame': self.window.start_frame_input.value(),
|
||||||
'start_frame': self.start_frame_input.value(),
|
'end_frame': self.window.end_frame_input.value(),
|
||||||
'end_frame': self.end_frame_input.value(),
|
'priority': self.window.priority_input.currentIndex() + 1,
|
||||||
'priority': self.priority_input.currentIndex() + 1,
|
'notes': self.window.notes_input.toPlainText(),
|
||||||
'notes': self.notes_input.toPlainText(),
|
'enable_split_jobs': self.window.enable_splitjobs.isChecked(),
|
||||||
'enable_split_jobs': self.enable_splitjobs.isChecked()}
|
'split_jobs_same_os': self.window.splitjobs_same_os.isChecked()}
|
||||||
|
|
||||||
input_path = self.scene_file_input.text()
|
# get the dynamic args
|
||||||
|
for i in range(self.window.renderer_options_layout.count()):
|
||||||
|
item = self.window.renderer_options_layout.itemAt(i)
|
||||||
|
layout = item.layout() # get the layout
|
||||||
|
for x in range(layout.count()):
|
||||||
|
z = layout.itemAt(x)
|
||||||
|
widget = z.widget()
|
||||||
|
if isinstance(widget, QComboBox):
|
||||||
|
job_json['args'][self.window.current_engine_options[i]['name']] = widget.currentText()
|
||||||
|
elif isinstance(widget, QLineEdit):
|
||||||
|
job_json['args'][self.window.current_engine_options[i]['name']] = widget.text()
|
||||||
|
|
||||||
|
# determine if any cameras are checked
|
||||||
|
selected_cameras = []
|
||||||
|
if self.window.cameras_list.count() and not self.window.cameras_group.isHidden():
|
||||||
|
for index in range(self.window.cameras_list.count()):
|
||||||
|
item = self.window.cameras_list.item(index)
|
||||||
|
if item.checkState() == Qt.CheckState.Checked:
|
||||||
|
selected_cameras.append(item.text().rsplit('-', 1)[0].strip()) # cleanup to just camera name
|
||||||
|
|
||||||
|
# process cameras into nested format
|
||||||
|
input_path = self.window.scene_file_input.text()
|
||||||
|
if selected_cameras:
|
||||||
|
job_list = []
|
||||||
|
for cam in selected_cameras:
|
||||||
|
job_copy = copy.deepcopy(job_json)
|
||||||
|
job_copy['args']['camera'] = cam
|
||||||
|
job_copy['name'] = pathlib.Path(input_path).stem.replace(' ', '_') + "-" + cam.replace(' ', '')
|
||||||
|
job_list.append(job_copy)
|
||||||
|
else:
|
||||||
job_list = [job_json]
|
job_list = [job_json]
|
||||||
self.submit_progress.setMaximum(0)
|
|
||||||
result = self.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list,
|
# presubmission tasks
|
||||||
|
engine = EngineManager.engine_with_name(self.window.renderer_type.currentText().lower())
|
||||||
|
input_path = engine().perform_presubmission_tasks(input_path)
|
||||||
|
# submit
|
||||||
|
result = self.window.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list,
|
||||||
callback=create_callback)
|
callback=create_callback)
|
||||||
self.submit_progress.setMaximum(0)
|
self.message_signal.emit(result)
|
||||||
|
|
||||||
# todo: error handle
|
|
||||||
print(result.json())
|
|
||||||
self.submit_button.setHidden(False)
|
|
||||||
self.submit_progress.setHidden(True)
|
|
||||||
self.submit_progress_label.setHidden(True)
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetProjectInfoWorker(QThread):
|
||||||
|
"""Worker class called to retrieve information about a project file on a background thread and update the UI"""
|
||||||
|
|
||||||
# submit thread
|
message_signal = pyqtSignal()
|
||||||
worker_thread = threading.Thread(target=submit_job_worker)
|
|
||||||
worker_thread.start()
|
def __init__(self, window, project_path):
|
||||||
|
super().__init__()
|
||||||
|
self.window = window
|
||||||
|
self.project_path = project_path
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
engine = EngineManager.engine_for_project_path(self.project_path)
|
||||||
|
self.window.project_info = engine().get_project_info(self.project_path)
|
||||||
|
self.message_signal.emit()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_layout(layout):
|
||||||
|
if layout is not None:
|
||||||
|
# Go through the layout's items in reverse order
|
||||||
|
for i in reversed(range(layout.count())):
|
||||||
|
# Take the item at the current position
|
||||||
|
item = layout.takeAt(i)
|
||||||
|
|
||||||
|
# Check if the item is a widget
|
||||||
|
if item.widget():
|
||||||
|
# Remove the widget and delete it
|
||||||
|
widget_to_remove = item.widget()
|
||||||
|
widget_to_remove.setParent(None)
|
||||||
|
widget_to_remove.deleteLater()
|
||||||
|
elif item.layout():
|
||||||
|
# If the item is a sub-layout, clear its contents recursively
|
||||||
|
clear_layout(item.layout())
|
||||||
|
# Then delete the layout
|
||||||
|
item.layout().deleteLater()
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from PyQt6.QtWidgets import (
|
|||||||
|
|
||||||
from src.api.server_proxy import RenderServerProxy
|
from src.api.server_proxy import RenderServerProxy
|
||||||
from src.engines.engine_manager import EngineManager
|
from src.engines.engine_manager import EngineManager
|
||||||
|
from src.utilities.misc_helper import is_localhost
|
||||||
|
|
||||||
|
|
||||||
class EngineBrowserWindow(QMainWindow):
|
class EngineBrowserWindow(QMainWindow):
|
||||||
@@ -84,7 +85,7 @@ class EngineBrowserWindow(QMainWindow):
|
|||||||
def update_table(self):
|
def update_table(self):
|
||||||
|
|
||||||
def update_table_worker():
|
def update_table_worker():
|
||||||
raw_server_data = RenderServerProxy(self.hostname).get_renderer_info(simple=True)
|
raw_server_data = RenderServerProxy(self.hostname).get_renderer_info()
|
||||||
if not raw_server_data:
|
if not raw_server_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -116,15 +117,16 @@ class EngineBrowserWindow(QMainWindow):
|
|||||||
def engine_picked(self):
|
def engine_picked(self):
|
||||||
engine_info = self.engine_data[self.table_widget.currentRow()]
|
engine_info = self.engine_data[self.table_widget.currentRow()]
|
||||||
self.delete_button.setEnabled(engine_info['type'] == 'managed')
|
self.delete_button.setEnabled(engine_info['type'] == 'managed')
|
||||||
self.launch_button.setEnabled(self.hostname == socket.gethostname())
|
self.launch_button.setEnabled(is_localhost(self.hostname))
|
||||||
|
|
||||||
def update_download_status(self):
|
def update_download_status(self):
|
||||||
hide_progress = not bool(EngineManager.download_tasks)
|
running_tasks = [x for x in EngineManager.download_tasks if x.is_alive()]
|
||||||
|
hide_progress = not bool(running_tasks)
|
||||||
self.progress_bar.setHidden(hide_progress)
|
self.progress_bar.setHidden(hide_progress)
|
||||||
self.progress_label.setHidden(hide_progress)
|
self.progress_label.setHidden(hide_progress)
|
||||||
|
|
||||||
# todo: update progress bar with status
|
# todo: update progress bar with status
|
||||||
self.progress_label.setText(f"Downloading {len(EngineManager.download_tasks)} engines")
|
self.progress_label.setText(f"Downloading {len(running_tasks)} engines")
|
||||||
|
|
||||||
def launch_button_click(self):
|
def launch_button_click(self):
|
||||||
engine_info = self.engine_data[self.table_widget.currentRow()]
|
engine_info = self.engine_data[self.table_widget.currentRow()]
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ from PIL import Image
|
|||||||
from PyQt6.QtCore import Qt, QByteArray, QBuffer, QIODevice, QThread
|
from PyQt6.QtCore import Qt, QByteArray, QBuffer, QIODevice, QThread
|
||||||
from PyQt6.QtGui import QPixmap, QImage, QFont, QIcon
|
from PyQt6.QtGui import QPixmap, QImage, QFont, QIcon
|
||||||
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QListWidget, QTableWidget, QAbstractItemView, \
|
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QListWidget, QTableWidget, QAbstractItemView, \
|
||||||
QTableWidgetItem, QLabel, QVBoxLayout, QHeaderView, QMessageBox, QGroupBox, QPushButton, QListWidgetItem
|
QTableWidgetItem, QLabel, QVBoxLayout, QHeaderView, QMessageBox, QGroupBox, QPushButton, QListWidgetItem, \
|
||||||
|
QFileDialog
|
||||||
|
|
||||||
from src.api.server_proxy import RenderServerProxy
|
from src.api.server_proxy import RenderServerProxy
|
||||||
from src.render_queue import RenderQueue
|
from src.render_queue import RenderQueue
|
||||||
from src.utilities.misc_helper import get_time_elapsed, resources_dir
|
from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost
|
||||||
from src.utilities.status_utils import RenderStatus
|
from src.utilities.status_utils import RenderStatus
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
from .add_job import NewRenderJobForm
|
from .add_job import NewRenderJobForm
|
||||||
@@ -292,7 +293,7 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Error fetching image: {e}")
|
logger.error(f"Error fetching image: {e}")
|
||||||
|
|
||||||
job_id = self.selected_job_ids()[0] if self.selected_job_ids() else None
|
job_id = self.selected_job_ids()[0] if self.selected_job_ids() else None
|
||||||
local_server = self.current_hostname == socket.gethostname()
|
local_server = is_localhost(self.current_hostname)
|
||||||
|
|
||||||
if job_id:
|
if job_id:
|
||||||
fetch_thread = threading.Thread(target=fetch_preview, args=(job_id,))
|
fetch_thread = threading.Thread(target=fetch_preview, args=(job_id,))
|
||||||
@@ -380,11 +381,12 @@ class MainWindow(QMainWindow):
|
|||||||
def update_servers(self):
|
def update_servers(self):
|
||||||
found_servers = list(set(ZeroconfServer.found_hostnames() + self.added_hostnames))
|
found_servers = list(set(ZeroconfServer.found_hostnames() + self.added_hostnames))
|
||||||
# Always make sure local hostname is first
|
# Always make sure local hostname is first
|
||||||
current_hostname = socket.gethostname()
|
if found_servers and not is_localhost(found_servers[0]):
|
||||||
if found_servers and found_servers[0] != current_hostname:
|
for hostname in found_servers:
|
||||||
if current_hostname in found_servers:
|
if is_localhost(hostname):
|
||||||
found_servers.remove(current_hostname)
|
found_servers.remove(hostname)
|
||||||
found_servers.insert(0, current_hostname)
|
found_servers.insert(0, hostname)
|
||||||
|
break
|
||||||
|
|
||||||
old_count = self.server_list_view.count()
|
old_count = self.server_list_view.count()
|
||||||
|
|
||||||
@@ -555,5 +557,8 @@ class MainWindow(QMainWindow):
|
|||||||
raise OSError("Unsupported operating system")
|
raise OSError("Unsupported operating system")
|
||||||
|
|
||||||
def new_job(self) -> None:
|
def new_job(self) -> None:
|
||||||
self.new_job_window = NewRenderJobForm()
|
|
||||||
|
file_name, _ = QFileDialog.getOpenFileName(self, "Select Scene File")
|
||||||
|
if file_name:
|
||||||
|
self.new_job_window = NewRenderJobForm(file_name)
|
||||||
self.new_job_window.show()
|
self.new_job_window.show()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -135,3 +136,13 @@ def config_dir():
|
|||||||
config_directory = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
config_directory = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
'config')
|
'config')
|
||||||
return config_directory
|
return config_directory
|
||||||
|
|
||||||
|
|
||||||
|
def is_localhost(comparison_hostname):
|
||||||
|
# this is necessary because socket.gethostname() does not always include '.local' - This is a sanitized comparison
|
||||||
|
try:
|
||||||
|
comparison_hostname = comparison_hostname.lower().replace('.local', '')
|
||||||
|
local_hostname = socket.gethostname().lower().replace('.local', '')
|
||||||
|
return comparison_hostname == local_hostname
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import zeroconf
|
from pubsub import pub
|
||||||
from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceStateChange
|
from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceStateChange, NonUniqueNameException
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class ZeroconfServer:
|
|||||||
cls.service_info = info
|
cls.service_info = info
|
||||||
cls.zeroconf.register_service(info)
|
cls.zeroconf.register_service(info)
|
||||||
logger.info(f"Registered zeroconf service: {cls.service_info.name}")
|
logger.info(f"Registered zeroconf service: {cls.service_info.name}")
|
||||||
except zeroconf.NonUniqueNameException as e:
|
except NonUniqueNameException as e:
|
||||||
logger.error(f"Error establishing zeroconf: {e}")
|
logger.error(f"Error establishing zeroconf: {e}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -76,6 +76,7 @@ class ZeroconfServer:
|
|||||||
cls.client_cache[name] = info
|
cls.client_cache[name] = info
|
||||||
else:
|
else:
|
||||||
cls.client_cache.pop(name)
|
cls.client_cache.pop(name)
|
||||||
|
pub.sendMessage('zeroconf_state_change', hostname=name, state_change=state_change, info=info)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def found_hostnames(cls):
|
def found_hostnames(cls):
|
||||||
|
|||||||
Reference in New Issue
Block a user