8 Commits

Author SHA1 Message Date
brett f6ee57fb55 FFMPEG path fixes for ffprobe 2026-01-06 23:49:22 -06:00
brett 2ba99cee31 EngineManager renaming and refactoring 2026-01-06 23:48:49 -06:00
Brett Williams 13a82a540a More window improvements 2026-01-06 21:50:41 -06:00
Brett Williams e7cecf6009 Send resolution / fps data in job submission 2026-01-06 19:33:46 -06:00
Brett Williams 2fdabd3a9d Cleanup Blender job creation 2026-01-06 03:45:19 -06:00
Brett Williams 2691c759ad Improve time representation in main window 2026-01-04 20:21:37 -06:00
Brett Williams a036b8244f Improved project naming and fixed Blender engine issue 2026-01-04 20:05:49 -06:00
Brett Williams 7b0d9a0b9f Initial refactor of add_job_window 2026-01-04 19:42:47 -06:00
11 changed files with 379 additions and 202 deletions
+9 -3
View File
@@ -309,6 +309,12 @@ def add_job_handler():
new_job = DistributedJobManager.create_render_job(processed_job_data, loaded_project_local_path) new_job = DistributedJobManager.create_render_job(processed_job_data, loaded_project_local_path)
created_jobs.append(new_job) created_jobs.append(new_job)
# Save notes to .txt
if processed_job_data.get("notes"):
parent_dir = os.path.dirname(os.path.dirname(loaded_project_local_path))
notes_name = processed_job_data['name'] + "-notes.txt"
with open(os.path.join(parent_dir, notes_name), "w") as f:
f.write(processed_job_data["notes"])
return [x.json() for x in created_jobs] return [x.json() for x in created_jobs]
except Exception as e: except Exception as e:
logger.exception(f"Error creating render job: {e}") logger.exception(f"Error creating render job: {e}")
@@ -383,7 +389,7 @@ def engine_info():
def process_engine(engine): def process_engine(engine):
try: try:
# Get all installed versions of the engine # Get all installed versions of the engine
installed_versions = EngineManager.all_versions_for_engine(engine.name()) installed_versions = EngineManager.all_version_data_for_engine(engine.name())
if not installed_versions: if not installed_versions:
return None return None
@@ -414,7 +420,7 @@ def engine_info():
except Exception as e: except Exception as e:
logger.error(f"Error fetching details for engine '{engine.name()}': {e}") logger.error(f"Error fetching details for engine '{engine.name()}': {e}")
raise e return {}
engine_data = {} engine_data = {}
with concurrent.futures.ThreadPoolExecutor() as executor: with concurrent.futures.ThreadPoolExecutor() as executor:
@@ -432,7 +438,7 @@ def engine_info():
def is_engine_available(engine_name): def is_engine_available(engine_name):
return {'engine': engine_name, 'available': RenderQueue.is_available_for_job(engine_name), return {'engine': engine_name, 'available': RenderQueue.is_available_for_job(engine_name),
'cpu_count': int(psutil.cpu_count(logical=False)), 'cpu_count': int(psutil.cpu_count(logical=False)),
'versions': EngineManager.all_versions_for_engine(engine_name), 'versions': EngineManager.all_version_data_for_engine(engine_name),
'hostname': server.config['HOSTNAME']} 'hostname': server.config['HOSTNAME']}
+2 -2
View File
@@ -43,9 +43,9 @@ class JobImportHandler:
raise FileNotFoundError("Cannot find any valid project paths") raise FileNotFoundError("Cannot find any valid project paths")
# Prepare the local filepath # Prepare the local filepath
cleaned_path_name = os.path.splitext(referred_name)[0].replace(' ', '-') cleaned_path_name = job_name.replace(' ', '-')
job_dir = os.path.join(upload_directory, '-'.join( job_dir = os.path.join(upload_directory, '-'.join(
[datetime.now().strftime("%Y.%m.%d_%H.%M.%S"), engine_name, cleaned_path_name])) [cleaned_path_name, engine_name, datetime.now().strftime("%Y.%m.%d_%H.%M.%S")]))
os.makedirs(job_dir, exist_ok=True) os.makedirs(job_dir, exist_ok=True)
project_source_dir = os.path.join(job_dir, 'source') project_source_dir = os.path.join(job_dir, 'source')
os.makedirs(project_source_dir, exist_ok=True) os.makedirs(project_source_dir, exist_ok=True)
+3 -2
View File
@@ -117,7 +117,7 @@ class Blender(BaseRenderEngine):
# report any missing textures # report any missing textures
not_found = re.findall("(Unable to pack file, source path .*)\n", result_text) not_found = re.findall("(Unable to pack file, source path .*)\n", result_text)
for err in not_found: for err in not_found:
logger.error(err) raise ChildProcessError(err)
p = re.compile('Saved to: (.*)\n') p = re.compile('Saved to: (.*)\n')
match = p.search(result_text) match = p.search(result_text)
@@ -125,6 +125,7 @@ class Blender(BaseRenderEngine):
new_path = os.path.join(dir_name, match.group(1).strip()) new_path = os.path.join(dir_name, match.group(1).strip())
logger.info(f'Blender file packed successfully to {new_path}') logger.info(f'Blender file packed successfully to {new_path}')
return new_path return new_path
return project_path
except Exception as e: except Exception as e:
msg = f'Error packing .blend file: {e}' msg = f'Error packing .blend file: {e}'
logger.error(msg) logger.error(msg)
@@ -164,7 +165,7 @@ class Blender(BaseRenderEngine):
return options return options
def system_info(self): def system_info(self):
return {'render_devices': self.get_render_devices()} return {'render_devices': self.get_render_devices(), 'engines': self.supported_render_engines()}
def get_render_devices(self): def get_render_devices(self):
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_system_info.py') script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_system_info.py')
+23 -9
View File
@@ -18,14 +18,13 @@ 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_project_info(input_path) self.scene_info = {}
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.project_length = (self.end_frame - self.start_frame) + 1
self.current_frame = -1 self.current_frame = -1
def generate_worker_subprocess(self): def generate_worker_subprocess(self):
self.scene_info = Blender(self.engine_path).get_project_info(self.input_path)
cmd = [self.engine_path] cmd = [self.engine_path]
if self.args.get('background', True): # optionally run render not in background if self.args.get('background', True): # optionally run render not in background
cmd.append('-b') cmd.append('-b')
@@ -41,10 +40,16 @@ class BlenderRenderWorker(BaseRenderWorker):
cmd.append('--python-expr') cmd.append('--python-expr')
python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;' python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;'
# Setup Custom Resolution
if self.args.get('resolution'):
res = self.args.get('resolution')
python_exp += 'bpy.context.scene.render.resolution_percentage = 100;'
python_exp += f'bpy.context.scene.render.resolution_x={res[0]}; bpy.context.scene.render.resolution_y={res[1]};'
# Setup Custom Camera # Setup Custom Camera
custom_camera = self.args.get('camera', None) custom_camera = self.args.get('camera', None)
if custom_camera: if custom_camera:
python_exp = python_exp + f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];" python_exp += f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];"
# Set Render Device for Cycles (gpu/cpu/any) # Set Render Device for Cycles (gpu/cpu/any)
if blender_engine == 'CYCLES': if blender_engine == 'CYCLES':
@@ -85,11 +90,15 @@ class BlenderRenderWorker(BaseRenderWorker):
def _parse_stdout(self, line): def _parse_stdout(self, line):
pattern = re.compile( cycles_pattern = re.compile(
r'Fra:(?P<frame>\d*).*Mem:(?P<memory>\S+).*Time:(?P<time>\S+)(?:.*Remaining:)?(?P<remaining>\S*)') r'Fra:(?P<frame>\d*).*Mem:(?P<memory>\S+).*Time:(?P<time>\S+)(?:.*Remaining:)?(?P<remaining>\S*)')
found = pattern.search(line) cycles_match = cycles_pattern.search(line)
if found: eevee_pattern = re.compile(
stats = found.groupdict() r"Rendering\s+(?P<current>\d+)\s*/\s*(?P<total>\d+)\s+samples"
)
eevee_match = eevee_pattern.search(line)
if cycles_match:
stats = cycles_match.groupdict()
memory_use = stats['memory'] memory_use = stats['memory']
time_elapsed = stats['time'] time_elapsed = stats['time']
time_remaining = stats['remaining'] or 'Unknown' time_remaining = stats['remaining'] or 'Unknown'
@@ -104,6 +113,11 @@ class BlenderRenderWorker(BaseRenderWorker):
logger.debug( logger.debug(
'Frame:{0} | Mem:{1} | Time:{2} | Remaining:{3}'.format(self.current_frame, memory_use, 'Frame:{0} | Mem:{1} | Time:{2} | Remaining:{3}'.format(self.current_frame, memory_use,
time_elapsed, time_remaining)) time_elapsed, time_remaining))
elif eevee_match:
self.__frame_percent_complete = int(eevee_match.groups()[0]) / int(eevee_match.groups()[1])
logger.debug(f'Frame:{self.current_frame} | Samples:{eevee_match.groups()[0]}/{eevee_match.groups()[1]}')
elif "Rendering frame" in line: # used for eevee
self.current_frame = int(line.split("Rendering frame")[-1].strip())
elif "file doesn't exist" in line.lower(): elif "file doesn't exist" in line.lower():
self.log_error(line, halt_render=True) self.log_error(line, halt_render=True)
elif line.lower().startswith('error'): elif line.lower().startswith('error'):
+1 -3
View File
@@ -36,7 +36,6 @@ class BaseRenderWorker(Base):
engine_version = Column(String) engine_version = Column(String)
engine_path = Column(String) engine_path = Column(String)
priority = Column(Integer) priority = Column(Integer)
project_length = Column(Integer)
start_frame = Column(Integer) start_frame = Column(Integer)
end_frame = Column(Integer, nullable=True) end_frame = Column(Integer, nullable=True)
parent = Column(String, nullable=True) parent = Column(String, nullable=True)
@@ -83,7 +82,6 @@ class BaseRenderWorker(Base):
self.maximum_attempts = 3 self.maximum_attempts = 3
# Frame Ranges # Frame Ranges
self.project_length = 0 # is this necessary?
self.current_frame = 0 self.current_frame = 0
self.start_frame = 0 self.start_frame = 0
self.end_frame = None self.end_frame = None
@@ -154,7 +152,7 @@ class BaseRenderWorker(Base):
@property @property
def total_frames(self): def total_frames(self):
return (self.end_frame or self.project_length) - self.start_frame + 1 return max(self.end_frame, 1) - self.start_frame + 1
@property @property
def status(self): def status(self):
+69 -51
View File
@@ -11,6 +11,9 @@ from src.utilities.misc_helper import system_safe_path, current_system_os, curre
logger = logging.getLogger() logger = logging.getLogger()
ENGINE_CLASSES = [Blender, FFMPEG]
class EngineManager: class EngineManager:
"""Class that manages different versions of installed render engines and handles fetching and downloading new versions, """Class that manages different versions of installed render engines and handles fetching and downloading new versions,
if possible. if possible.
@@ -21,32 +24,36 @@ class EngineManager:
@staticmethod @staticmethod
def supported_engines(): def supported_engines():
return [Blender, FFMPEG] return ENGINE_CLASSES
# --- Installed Engines ---
@classmethod @classmethod
def downloadable_engines(cls): def engine_for_project_path(cls, path):
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()] _, extension = os.path.splitext(path)
extension = extension.lower().strip('.')
@classmethod for engine_class in cls.supported_engines():
def active_downloads(cls) -> list: engine = cls.get_latest_engine_instance(engine_class)
return [x for x in cls.download_tasks if x.is_alive()] if extension in engine.supported_extensions():
return engine
undefined_renderer_support = [x for x in cls.supported_engines() if not cls.get_latest_engine_instance(x).supported_extensions()]
return undefined_renderer_support[0]
@classmethod @classmethod
def engine_with_name(cls, engine_name): def engine_with_name(cls, engine_name):
for obj in cls.supported_engines(): for obj in cls.supported_engines():
if obj.name().lower() == engine_name.lower(): if obj.name().lower() == engine_name.lower():
return obj return obj
return None
@classmethod @classmethod
def update_all_engines(cls): def get_latest_engine_instance(cls, engine_class):
for engine in cls.downloadable_engines(): newest = cls.newest_installed_engine_data(engine_class.name())
update_available = cls.is_engine_update_available(engine) engine = engine_class(newest["path"])
if update_available: return engine
update_available['name'] = engine.name()
cls.download_engine(engine.name(), update_available['version'], background=True)
@classmethod @classmethod
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False): def get_installed_engine_data(cls, filter_name=None, include_corrupt=False, ignore_system=False):
if not cls.engines_path: if not cls.engines_path:
raise FileNotFoundError("Engine path is not set") raise FileNotFoundError("Engine path is not set")
@@ -123,31 +130,41 @@ class EngineManager:
return results return results
# --- Check for Updates ---
@classmethod @classmethod
def all_versions_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False): def update_all_engines(cls):
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system) for engine in cls.downloadable_engines():
update_available = cls.is_engine_update_available(engine)
if update_available:
update_available['name'] = engine.name()
cls.download_engine(engine.name(), update_available['version'], background=True)
@classmethod
def all_version_data_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False):
versions = cls.get_installed_engine_data(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system)
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True) sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
return sorted_versions return sorted_versions
@classmethod @classmethod
def newest_engine_version(cls, engine, system_os=None, cpu=None, ignore_system=None): def newest_installed_engine_data(cls, engine_name, system_os=None, cpu=None, ignore_system=None):
system_os = system_os or current_system_os() system_os = system_os or current_system_os()
cpu = cpu or current_system_cpu() cpu = cpu or current_system_cpu()
try: try:
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system) filtered = [x for x in cls.all_version_data_for_engine(engine_name, ignore_system=ignore_system)
if x['system_os'] == system_os and x['cpu'] == cpu] if x['system_os'] == system_os and x['cpu'] == cpu]
return filtered[0] return filtered[0]
except IndexError: except IndexError:
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}") logger.error(f"Cannot find newest engine version for {engine_name}-{system_os}-{cpu}")
return None return None
@classmethod @classmethod
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False): def is_version_installed(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
system_os = system_os or current_system_os() system_os = system_os or current_system_os()
cpu = cpu or current_system_cpu() cpu = cpu or current_system_cpu()
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if filtered = [x for x in cls.get_installed_engine_data(filter_name=engine, ignore_system=ignore_system) if
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version] x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
return filtered[0] if filtered else False return filtered[0] if filtered else False
@@ -169,6 +186,28 @@ class EngineManager:
logger.debug(f"Exception in find_most_recent_version: {e}") logger.debug(f"Exception in find_most_recent_version: {e}")
return None return None
@classmethod
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
logger.debug(f"Checking for updates to {engine_class.name()}")
latest_version = engine_class.downloader().find_most_recent_version()
if not latest_version:
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
return None
version_num = latest_version.get('version')
if cls.is_version_installed(engine_class.name(), version_num, ignore_system=ignore_system_installs):
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
return None
return latest_version
# --- Downloads ---
@classmethod
def downloadable_engines(cls):
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
@classmethod @classmethod
def get_existing_download_task(cls, engine, version, system_os=None, cpu=None): def get_existing_download_task(cls, engine, version, system_os=None, cpu=None):
for task in cls.download_tasks: for task in cls.download_tasks:
@@ -204,7 +243,7 @@ class EngineManager:
return thread return thread
thread.join() thread.join()
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded found_engine = cls.is_version_installed(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
if not found_engine: if not found_engine:
logger.error(f"Error downloading {engine}") logger.error(f"Error downloading {engine}")
return found_engine return found_engine
@@ -213,7 +252,7 @@ class EngineManager:
def delete_engine_download(cls, engine, version, system_os=None, cpu=None): def delete_engine_download(cls, engine, version, system_os=None, cpu=None):
logger.info(f"Requested deletion of engine: {engine}-{version}") logger.info(f"Requested deletion of engine: {engine}-{version}")
found = cls.is_version_downloaded(engine, version, system_os, cpu) found = cls.is_version_installed(engine, version, system_os, cpu)
if found and found['type'] == 'managed': # don't delete system installs if found and found['type'] == 'managed': # don't delete system installs
# find the root directory of the engine executable # find the root directory of the engine executable
root_dir_name = '-'.join([engine, version, found['system_os'], found['cpu']]) root_dir_name = '-'.join([engine, version, found['system_os'], found['cpu']])
@@ -229,22 +268,11 @@ class EngineManager:
logger.error(f"Cannot find engine: {engine}-{version}") logger.error(f"Cannot find engine: {engine}-{version}")
return False return False
# --- Background Tasks ---
@classmethod @classmethod
def is_engine_update_available(cls, engine_class, ignore_system_installs=False): def active_downloads(cls) -> list:
logger.debug(f"Checking for updates to {engine_class.name()}") return [x for x in cls.download_tasks if x.is_alive()]
latest_version = engine_class.downloader().find_most_recent_version()
if not latest_version:
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
return None
version_num = latest_version.get('version')
if cls.is_version_downloaded(engine_class.name(), version_num, ignore_system=ignore_system_installs):
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
return None
return latest_version
@classmethod @classmethod
def create_worker(cls, engine_name, input_path, output_path, engine_version=None, args=None, parent=None, name=None): def create_worker(cls, engine_name, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
@@ -252,7 +280,7 @@ class EngineManager:
worker_class = cls.engine_with_name(engine_name).worker_class() worker_class = cls.engine_with_name(engine_name).worker_class()
# check to make sure we have versions installed # check to make sure we have versions installed
all_versions = cls.all_versions_for_engine(engine_name) all_versions = cls.all_version_data_for_engine(engine_name)
if not all_versions: if not all_versions:
raise FileNotFoundError(f"Cannot find any installed '{engine_name}' engines") raise FileNotFoundError(f"Cannot find any installed '{engine_name}' engines")
@@ -281,16 +309,6 @@ class EngineManager:
return worker_class(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args, return worker_class(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args,
parent=parent, name=name) parent=parent, name=name)
@classmethod
def engine_for_project_path(cls, path):
_, extension = os.path.splitext(path)
extension = extension.lower().strip('.')
for engine in cls.supported_engines():
if extension in engine().supported_extensions():
return engine
undefined_renderer_support = [x for x in cls.supported_engines() if not x().supported_extensions()]
return undefined_renderer_support[0]
class EngineDownloadWorker(threading.Thread): class EngineDownloadWorker(threading.Thread):
"""A thread worker for downloading a specific version of a rendering engine. """A thread worker for downloading a specific version of a rendering engine.
@@ -317,7 +335,7 @@ class EngineDownloadWorker(threading.Thread):
def run(self): def run(self):
try: try:
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu, existing_download = EngineManager.is_version_installed(self.engine, self.version, self.system_os, self.cpu,
ignore_system=True) ignore_system=True)
if existing_download: if existing_download:
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists") logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
@@ -341,4 +359,4 @@ if __name__ == '__main__':
# EngineManager.delete_engine_download('blender', '3.2.1', 'macos', 'a') # EngineManager.delete_engine_download('blender', '3.2.1', 'macos', 'a')
EngineManager.engines_path = "/Users/brettwilliams/zordon-uploads/engines" EngineManager.engines_path = "/Users/brettwilliams/zordon-uploads/engines"
# print(EngineManager.is_version_downloaded("ffmpeg", "6.0")) # print(EngineManager.is_version_downloaded("ffmpeg", "6.0"))
print(EngineManager.get_engines()) print(EngineManager.get_installed_engine_data())
+16 -5
View File
@@ -1,4 +1,5 @@
import json import json
import os
import re import re
from src.engines.core.base_engine import * from src.engines.core.base_engine import *
@@ -19,9 +20,10 @@ class FFMPEG(BaseRenderEngine):
from src.engines.ffmpeg.ffmpeg_worker import FFMPEGRenderWorker from src.engines.ffmpeg.ffmpeg_worker import FFMPEGRenderWorker
return FFMPEGRenderWorker return FFMPEGRenderWorker
def ui_options(self): @staticmethod
def ui_options(system_info):
from src.engines.ffmpeg.ffmpeg_ui import FFMPEGUI from src.engines.ffmpeg.ffmpeg_ui import FFMPEGUI
return FFMPEGUI.get_options(self) return FFMPEGUI.get_options(system_info)
def supported_extensions(self): def supported_extensions(self):
help_text = (subprocess.check_output([self.engine_path(), '-h', 'full'], stderr=subprocess.STDOUT, help_text = (subprocess.check_output([self.engine_path(), '-h', 'full'], stderr=subprocess.STDOUT,
@@ -45,10 +47,19 @@ class FFMPEG(BaseRenderEngine):
return version return version
def get_project_info(self, project_path, timeout=10): def get_project_info(self, project_path, timeout=10):
"""Run ffprobe and parse the output as JSON"""
try: try:
# Run ffprobe and parse the output as JSON # resolve ffprobe path
engine_dir = os.path.dirname(self.engine_path())
ffprobe_path = os.path.join(engine_dir, 'ffprobe')
if self.engine_path().endswith('.exe'):
ffprobe_path += '.exe'
if not os.path.exists(ffprobe_path): # fallback to system install (if available)
ffprobe_path = 'ffprobe'
# run ffprobe
cmd = [ cmd = [
'ffprobe', '-v', 'quiet', '-print_format', 'json', ffprobe_path, '-v', 'quiet', '-print_format', 'json',
'-show_streams', '-select_streams', 'v', project_path '-show_streams', '-select_streams', 'v', project_path
] ]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True, output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True,
@@ -78,7 +89,7 @@ class FFMPEG(BaseRenderEngine):
} }
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") print(f"Failed to get FFMPEG project info: {e}")
return None return None
def get_encoders(self): def get_encoders(self):
+2 -2
View File
@@ -19,8 +19,8 @@ class FFMPEGRenderWorker(BaseRenderWorker):
cmd = [self.engine_path, '-y', '-stats', '-i', self.input_path] cmd = [self.engine_path, '-y', '-stats', '-i', self.input_path]
# Resize frame # Resize frame
if self.args.get('x_resolution', None) and self.args.get('y_resolution', None): if self.args.get('resolution', None):
cmd.extend(['-vf', f"scale={self.args['x_resolution']}:{self.args['y_resolution']}"]) cmd.extend(['-vf', f"scale={self.args['resolution'][0]}:{self.args['resolution'][1]}"])
# Convert raw args from string if available # Convert raw args from string if available
raw_args = self.args.get('raw', None) raw_args = self.args.get('raw', None)
+190 -101
View File
@@ -1,32 +1,33 @@
import copy
import os.path import os.path
import pathlib
import socket import socket
import threading
import psutil import psutil
from PyQt6.QtCore import QThread, pyqtSignal, Qt, pyqtSlot from PyQt6.QtCore import QThread, pyqtSignal, Qt, pyqtSlot
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, QDoubleSpinBox, QMessageBox, QListWidget, QListWidgetItem QGroupBox, QCheckBox, QProgressBar, QPlainTextEdit, QDoubleSpinBox, QMessageBox, QListWidget, QListWidgetItem,
QTabWidget
) )
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
from src.ui.engine_help_window import EngineHelpViewer from src.ui.engine_help_window import EngineHelpViewer
from src.utilities.zeroconf_server import ZeroconfServer from src.utilities.zeroconf_server import ZeroconfServer
from src.utilities.misc_helper import COMMON_RESOLUTIONS
from utilities.misc_helper import COMMON_FRAME_RATES
class NewRenderJobForm(QWidget): class NewRenderJobForm(QWidget):
def __init__(self, project_path=None): def __init__(self, project_path=None):
super().__init__() super().__init__()
self.notes_group = None self.resolution_options_list = None
self.frame_rate_input = None
self.resolution_x_input = None self.resolution_x_input = None
self.engine_group = None
self.output_settings_group = None
self.resolution_y_input = None self.resolution_y_input = None
self.fps_options_list = None
self.fps_input = None
self.engine_group = None
self.notes_group = None
self.output_settings_group = None
self.project_path = project_path self.project_path = project_path
# UI # UI
@@ -55,10 +56,10 @@ 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.render_name_input = None self.job_name_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.tabs = None
# Job / Server Data # Job / Server Data
self.server_proxy = RenderServerProxy(socket.gethostname()) self.server_proxy = RenderServerProxy(socket.gethostname())
@@ -78,129 +79,186 @@ class NewRenderJobForm(QWidget):
self.show() self.show()
def setup_ui(self): def setup_ui(self):
# Main Layout # Main widget layout
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
# Loading File Group # Tabs
self.tabs = QTabWidget()
# ==================== Loading Section (outside tabs) ====================
self.load_file_group = QGroupBox("Loading") self.load_file_group = QGroupBox("Loading")
load_file_layout = QVBoxLayout(self.load_file_group) load_file_layout = QVBoxLayout(self.load_file_group)
# progress bar
progress_layout = QHBoxLayout() progress_layout = QHBoxLayout()
self.process_label = QLabel("Processing")
self.process_progress_bar = QProgressBar() self.process_progress_bar = QProgressBar()
self.process_progress_bar.setMinimum(0) self.process_progress_bar.setMinimum(0)
self.process_progress_bar.setMaximum(0) self.process_progress_bar.setMaximum(0) # Indeterminate
self.process_label = QLabel("Processing")
progress_layout.addWidget(self.process_label) progress_layout.addWidget(self.process_label)
progress_layout.addWidget(self.process_progress_bar) progress_layout.addWidget(self.process_progress_bar)
load_file_layout.addLayout(progress_layout) load_file_layout.addLayout(progress_layout)
main_layout.addWidget(self.load_file_group)
# Project Group # Scene File
self.project_group = QGroupBox("Project") job_overview_group = QGroupBox("Project File")
server_layout = QVBoxLayout(self.project_group) file_group_layout = QVBoxLayout(job_overview_group)
# File Path
# Job Name
job_name_layout = QHBoxLayout()
job_name_layout.addWidget(QLabel("Job name:"))
self.job_name_input = QLineEdit()
job_name_layout.addWidget(self.job_name_input)
file_group_layout.addLayout(job_name_layout)
# Job File
scene_file_picker_layout = QHBoxLayout() scene_file_picker_layout = QHBoxLayout()
scene_file_picker_layout.addWidget(QLabel("File:"))
self.scene_file_input = QLineEdit() self.scene_file_input = QLineEdit()
self.scene_file_input.setText(self.project_path) 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(QLabel("File:"))
scene_file_picker_layout.addWidget(self.scene_file_input) scene_file_picker_layout.addWidget(self.scene_file_input)
scene_file_picker_layout.addWidget(self.scene_file_browse_button) scene_file_picker_layout.addWidget(self.scene_file_browse_button)
server_layout.addLayout(scene_file_picker_layout) file_group_layout.addLayout(scene_file_picker_layout)
# Server List
main_layout.addWidget(job_overview_group)
main_layout.addWidget(self.load_file_group)
main_layout.addWidget(self.tabs)
# ==================== Tab 1: Job Settings ====================
self.project_group = QWidget()
project_layout = QVBoxLayout(self.project_group) # Fixed: proper parent
# Server / Hostname
server_list_layout = QHBoxLayout() server_list_layout = QHBoxLayout()
server_list_layout.setSpacing(0) server_list_layout.addWidget(QLabel("Render Target:"))
self.server_input = QComboBox() self.server_input = QComboBox()
server_list_layout.addWidget(QLabel("Hostname:"), 1) server_list_layout.addWidget(self.server_input)
server_list_layout.addWidget(self.server_input, 3) project_layout.addLayout(server_list_layout)
server_layout.addLayout(server_list_layout)
main_layout.addWidget(self.project_group)
self.update_server_list()
# Priority # Priority
priority_layout = QHBoxLayout() priority_layout = QHBoxLayout()
priority_layout.addWidget(QLabel("Priority:"), 1) priority_layout.addWidget(QLabel("Priority:"))
self.priority_input = QComboBox() self.priority_input = QComboBox()
self.priority_input.addItems(["High", "Medium", "Low"]) self.priority_input.addItems(["High", "Medium", "Low"])
self.priority_input.setCurrentIndex(1) self.priority_input.setCurrentIndex(1)
priority_layout.addWidget(self.priority_input, 3) priority_layout.addWidget(self.priority_input)
server_layout.addLayout(priority_layout) project_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 # Split Jobs Options
self.output_settings_group = QGroupBox("Output Settings") self.enable_splitjobs = QCheckBox("Automatically split render across multiple servers")
project_layout.addWidget(self.enable_splitjobs)
self.splitjobs_same_os = QCheckBox("Only render on same OS")
project_layout.addWidget(self.splitjobs_same_os)
project_layout.addStretch() # Push everything up
# ==================== Tab 2: Output Settings ====================
self.output_settings_group = QWidget()
output_settings_layout = QVBoxLayout(self.output_settings_group) output_settings_layout = QVBoxLayout(self.output_settings_group)
# output path
render_name_layout = QHBoxLayout() # File Format
render_name_layout.addWidget(QLabel("Render name:")) format_group = QGroupBox("Format / Range")
self.render_name_input = QLineEdit() output_settings_layout.addWidget(format_group)
render_name_layout.addWidget(self.render_name_input) format_group_layout = QVBoxLayout()
output_settings_layout.addLayout(render_name_layout) format_group.setLayout(format_group_layout)
# file format
file_format_layout = QHBoxLayout() file_format_layout = QHBoxLayout()
file_format_layout.addWidget(QLabel("Format:")) file_format_layout.addWidget(QLabel("Format:"))
self.file_format_combo = QComboBox() self.file_format_combo = QComboBox()
self.file_format_combo.setFixedWidth(200)
file_format_layout.addWidget(self.file_format_combo) file_format_layout.addWidget(self.file_format_combo)
output_settings_layout.addLayout(file_format_layout) file_format_layout.addStretch()
# frame range format_group_layout.addLayout(file_format_layout)
frame_range_layout = QHBoxLayout(self.output_settings_group)
# Frame Range
frame_range_layout = QHBoxLayout()
frame_range_layout.addWidget(QLabel("Frames:"))
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.start_frame_input.setFixedWidth(80)
self.end_frame_input = QSpinBox() self.end_frame_input = QSpinBox()
self.end_frame_input.setRange(1, 99999) self.end_frame_input.setRange(1, 99999)
frame_range_layout.addWidget(QLabel("Frames:")) self.end_frame_input.setFixedWidth(80)
frame_range_layout.addWidget(self.start_frame_input) frame_range_layout.addWidget(self.start_frame_input)
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) frame_range_layout.addStretch()
# resolution format_group_layout.addLayout(frame_range_layout)
resolution_layout = QHBoxLayout(self.output_settings_group)
# --- Resolution & FPS Group ---
resolution_group = QGroupBox("Resolution / Frame Rate")
output_settings_layout.addWidget(resolution_group)
resolution_group_layout = QVBoxLayout()
resolution_group.setLayout(resolution_group_layout)
# Resolution
resolution_layout = QHBoxLayout(resolution_group)
self.resolution_options_list = QComboBox()
self.resolution_options_list.setFixedWidth(200)
self.resolution_options_list.addItem("Original Size")
for res in COMMON_RESOLUTIONS:
self.resolution_options_list.addItem(res)
self.resolution_options_list.currentIndexChanged.connect(self._resolution_preset_changed)
resolution_layout.addWidget(self.resolution_options_list)
resolution_group_layout.addLayout(resolution_layout)
self.resolution_x_input = QSpinBox() self.resolution_x_input = QSpinBox()
self.resolution_x_input.setRange(1, 9999) # Assuming max resolution width 9999 self.resolution_x_input.setRange(1, 9999)
self.resolution_x_input.setValue(1920) self.resolution_x_input.setValue(1920)
self.resolution_y_input = QSpinBox() self.resolution_x_input.setFixedWidth(80)
self.resolution_y_input.setRange(1, 9999) # Assuming max resolution height 9999
self.resolution_y_input.setValue(1080)
self.frame_rate_input = QDoubleSpinBox()
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(self.resolution_x_input)
self.resolution_y_input = QSpinBox()
self.resolution_y_input.setRange(1, 9999)
self.resolution_y_input.setValue(1080)
self.resolution_y_input.setFixedWidth(80)
resolution_layout.addWidget(QLabel("x")) resolution_layout.addWidget(QLabel("x"))
resolution_layout.addWidget(self.resolution_y_input) resolution_layout.addWidget(self.resolution_y_input)
resolution_layout.addWidget(QLabel("@")) resolution_layout.addStretch()
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)
# Engine Group fps_layout = QHBoxLayout(resolution_group)
self.engine_group = QGroupBox("Engine Settings") self.fps_options_list = QComboBox()
self.fps_options_list.setFixedWidth(200)
self.fps_options_list.addItem("Original FPS")
for fps_option in COMMON_FRAME_RATES:
self.fps_options_list.addItem(fps_option)
self.fps_options_list.currentIndexChanged.connect(self._fps_preset_changed)
fps_layout.addWidget(self.fps_options_list)
self.fps_input = QDoubleSpinBox()
self.fps_input.setDecimals(3)
self.fps_input.setRange(1.0, 999.0)
self.fps_input.setValue(23.976)
self.fps_input.setFixedWidth(80)
fps_layout.addWidget(self.fps_input)
fps_layout.addWidget(QLabel("fps"))
fps_layout.addStretch()
resolution_group_layout.addLayout(fps_layout)
output_settings_layout.addStretch()
# ==================== Tab 3: Engine Settings ====================
self.engine_group = QWidget()
engine_group_layout = QVBoxLayout(self.engine_group) engine_group_layout = QVBoxLayout(self.engine_group)
engine_layout = QHBoxLayout() engine_layout = QHBoxLayout()
engine_layout.addWidget(QLabel("Engine:")) engine_layout.addWidget(QLabel("Engine:"))
self.engine_type = QComboBox() self.engine_type = QComboBox()
self.engine_type.currentIndexChanged.connect(self.engine_changed) self.engine_type.currentIndexChanged.connect(self.engine_changed)
engine_layout.addWidget(self.engine_type) engine_layout.addWidget(self.engine_type)
# Version
engine_layout.addWidget(QLabel("Version:")) engine_layout.addWidget(QLabel("Version:"))
self.engine_version_combo = QComboBox() self.engine_version_combo = QComboBox()
self.engine_version_combo.addItem('latest') self.engine_version_combo.addItem('latest')
engine_layout.addWidget(self.engine_version_combo) engine_layout.addWidget(self.engine_version_combo)
engine_group_layout.addLayout(engine_layout) engine_group_layout.addLayout(engine_layout)
# dynamic options
# Dynamic engine options
self.engine_options_layout = QVBoxLayout() self.engine_options_layout = QVBoxLayout()
engine_group_layout.addLayout(self.engine_options_layout) engine_group_layout.addLayout(self.engine_options_layout)
# Raw Args # Raw Args
raw_args_layout = QHBoxLayout(self.engine_group) raw_args_layout = QHBoxLayout()
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)
@@ -208,24 +266,33 @@ class NewRenderJobForm(QWidget):
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)
engine_group_layout.addLayout(raw_args_layout) engine_group_layout.addLayout(raw_args_layout)
main_layout.addWidget(self.engine_group) engine_group_layout.addStretch()
# Cameras Group # ==================== Tab 4: Cameras ====================
self.cameras_group = QGroupBox("Cameras") self.cameras_group = QWidget()
cameras_layout = QVBoxLayout(self.cameras_group) cameras_layout = QVBoxLayout(self.cameras_group)
self.cameras_list = QListWidget() self.cameras_list = QListWidget()
self.cameras_group.setHidden(True)
cameras_layout.addWidget(self.cameras_list) cameras_layout.addWidget(self.cameras_list)
main_layout.addWidget(self.cameras_group)
# Notes Group # ==================== Tab 5: Misc / Notes ====================
self.notes_group = QGroupBox("Additional Notes") self.notes_group = QWidget()
notes_layout = QVBoxLayout(self.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(self.notes_group)
# Submit Button # == Create Tabs
self.tabs.addTab(self.project_group, "Job Settings")
self.tabs.addTab(self.output_settings_group, "Output Settings")
self.tabs.addTab(self.engine_group, "Engine Settings")
self.tabs.addTab(self.cameras_group, "Cameras")
self.tabs.addTab(self.notes_group, "Notes")
self.update_server_list()
index = self.tabs.indexOf(self.cameras_group)
if index != -1:
self.tabs.setTabEnabled(index, False)
# ==================== Submit Section (outside tabs) ====================
self.submit_button = QPushButton("Submit Job") self.submit_button = QPushButton("Submit Job")
self.submit_button.clicked.connect(self.submit_job) self.submit_button.clicked.connect(self.submit_job)
main_layout.addWidget(self.submit_button) main_layout.addWidget(self.submit_button)
@@ -240,7 +307,25 @@ class NewRenderJobForm(QWidget):
self.submit_progress_label.setHidden(True) self.submit_progress_label.setHidden(True)
main_layout.addWidget(self.submit_progress_label) main_layout.addWidget(self.submit_progress_label)
# Initial engine state
self.toggle_engine_enablement(False) self.toggle_engine_enablement(False)
self.tabs.setCurrentIndex(0)
def _resolution_preset_changed(self, index):
selected_res = COMMON_RESOLUTIONS.get(self.resolution_options_list.currentText())
if selected_res:
self.resolution_x_input.setValue(selected_res[0])
self.resolution_y_input.setValue(selected_res[1])
elif index == 0:
self.resolution_x_input.setValue(self.project_info.get('resolution_x'))
self.resolution_y_input.setValue(self.project_info.get('resolution_y'))
def _fps_preset_changed(self, index):
selected_fps = COMMON_FRAME_RATES.get(self.fps_options_list.currentText())
if selected_fps:
self.fps_input.setValue(selected_fps)
elif index == 0:
self.fps_input.setValue(self.project_info.get('fps'))
def update_engine_info(self): def update_engine_info(self):
# get the engine info and add them all to the ui # get the engine info and add them all to the ui
@@ -282,7 +367,7 @@ class NewRenderJobForm(QWidget):
output_name, _ = os.path.splitext(os.path.basename(self.scene_file_input.text())) output_name, _ = os.path.splitext(os.path.basename(self.scene_file_input.text()))
output_name = output_name.replace(' ', '_') output_name = output_name.replace(' ', '_')
self.render_name_input.setText(output_name) self.job_name_input.setText(output_name)
file_name = self.scene_file_input.text() file_name = self.scene_file_input.text()
# setup bg worker # setup bg worker
@@ -293,7 +378,7 @@ class NewRenderJobForm(QWidget):
def browse_output_path(self): def browse_output_path(self):
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory") directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
if directory: if directory:
self.render_name_input.setText(directory) self.job_name_input.setText(directory)
def args_help_button_clicked(self): def args_help_button_clicked(self):
url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/engine/' url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/engine/'
@@ -326,12 +411,13 @@ class NewRenderJobForm(QWidget):
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_x_input.setValue(self.project_info.get('resolution_x'))
self.resolution_y_input.setValue(self.project_info.get('resolution_y')) self.resolution_y_input.setValue(self.project_info.get('resolution_y'))
self.frame_rate_input.setValue(self.project_info.get('fps')) self.fps_input.setValue(self.project_info.get('fps'))
# Cameras # Cameras
self.cameras_list.clear() self.cameras_list.clear()
index = self.tabs.indexOf(self.cameras_group)
if self.project_info.get('cameras'): if self.project_info.get('cameras'):
self.cameras_group.setHidden(False) self.tabs.setTabEnabled(index, True)
found_active = False found_active = False
for camera in self.project_info['cameras']: for camera in self.project_info['cameras']:
# create the list items and make them checkable # create the list items and make them checkable
@@ -344,13 +430,12 @@ class NewRenderJobForm(QWidget):
if not found_active: if not found_active:
self.cameras_list.item(0).setCheckState(Qt.CheckState.Checked) self.cameras_list.item(0).setCheckState(Qt.CheckState.Checked)
else: else:
self.cameras_group.setHidden(True) self.tabs.setTabEnabled(index, False)
# Dynamic Engine Options # Dynamic Engine Options
clear_layout(self.engine_options_layout) # clear old options clear_layout(self.engine_options_layout) # clear old options
# dynamically populate option list # dynamically populate option list
system_info = self.engine_info.get(engine.name(), {}).get('system_info', {}) self.current_engine_options = engine().ui_options()
self.current_engine_options = engine.ui_options(system_info=system_info)
for option in self.current_engine_options: for option in self.current_engine_options:
h_layout = QHBoxLayout() h_layout = QHBoxLayout()
label = QLabel(option['name'].replace('_', ' ').capitalize() + ':') label = QLabel(option['name'].replace('_', ' ').capitalize() + ':')
@@ -369,12 +454,13 @@ class NewRenderJobForm(QWidget):
def toggle_engine_enablement(self, enabled=False): def toggle_engine_enablement(self, enabled=False):
"""Toggle on/off all the render settings""" """Toggle on/off all the render settings"""
self.project_group.setHidden(not enabled) indexes = [self.tabs.indexOf(self.project_group),
self.output_settings_group.setHidden(not enabled) self.tabs.indexOf(self.output_settings_group),
self.engine_group.setHidden(not enabled) self.tabs.indexOf(self.engine_group),
self.notes_group.setHidden(not enabled) self.tabs.indexOf(self.cameras_group),
if not enabled: self.tabs.indexOf(self.notes_group)]
self.cameras_group.setHidden(True) for idx in indexes:
self.tabs.setTabEnabled(idx, enabled)
self.submit_button.setEnabled(enabled) self.submit_button.setEnabled(enabled)
def after_job_submission(self, error_string): def after_job_submission(self, error_string):
@@ -449,19 +535,22 @@ class SubmitWorker(QThread):
try: try:
hostname = self.window.server_input.currentText() hostname = self.window.server_input.currentText()
resolution = (self.window.resolution_x_input.text(), self.window.resolution_y_input.text())
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(), job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
'engine_name': self.window.engine_type.currentText().lower(), 'engine_name': self.window.engine_type.currentText().lower(),
'engine_version': self.window.engine_version_combo.currentText(), 'engine_version': self.window.engine_version_combo.currentText(),
'args': {'raw': self.window.raw_args.text(), 'args': {'raw': self.window.raw_args.text(),
'export_format': self.window.file_format_combo.currentText()}, 'export_format': self.window.file_format_combo.currentText(),
'output_path': self.window.render_name_input.text(), 'resolution': resolution,
'fps': self.window.fps_input.text(),},
'output_path': self.window.job_name_input.text(),
'start_frame': self.window.start_frame_input.value(), 'start_frame': self.window.start_frame_input.value(),
'end_frame': self.window.end_frame_input.value(), 'end_frame': self.window.end_frame_input.value(),
'priority': self.window.priority_input.currentIndex() + 1, 'priority': self.window.priority_input.currentIndex() + 1,
'notes': self.window.notes_input.toPlainText(), 'notes': self.window.notes_input.toPlainText(),
'enable_split_jobs': self.window.enable_splitjobs.isChecked(), 'enable_split_jobs': self.window.enable_splitjobs.isChecked(),
'split_jobs_same_os': self.window.splitjobs_same_os.isChecked(), 'split_jobs_same_os': self.window.splitjobs_same_os.isChecked(),
'name': self.window.render_name_input.text()} 'name': self.window.job_name_input.text()}
# get the dynamic args # get the dynamic args
for i in range(self.window.engine_options_layout.count()): for i in range(self.window.engine_options_layout.count()):
+10 -9
View File
@@ -2,7 +2,6 @@
import ast import ast
import datetime import datetime
import io import io
import json
import logging import logging
import os import os
import sys import sys
@@ -10,6 +9,7 @@ import threading
import time import time
import PIL import PIL
import humanize
from PIL import Image 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
@@ -18,10 +18,8 @@ from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QListWidget, QTab
QFileDialog QFileDialog
from src.api.api_server import API_VERSION from src.api.api_server import API_VERSION
from src.api.serverproxy_manager import ServerProxyManager
from src.render_queue import RenderQueue from src.render_queue import RenderQueue
from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost
from src.utilities.status_utils import RenderStatus
from src.utilities.zeroconf_server import ZeroconfServer
from src.ui.add_job_window import NewRenderJobForm from src.ui.add_job_window import NewRenderJobForm
from src.ui.console_window import ConsoleWindow from src.ui.console_window import ConsoleWindow
from src.ui.engine_browser import EngineBrowserWindow from src.ui.engine_browser import EngineBrowserWindow
@@ -30,8 +28,10 @@ from src.ui.widgets.menubar import MenuBar
from src.ui.widgets.proportional_image_label import ProportionalImageLabel from src.ui.widgets.proportional_image_label import ProportionalImageLabel
from src.ui.widgets.statusbar import StatusBar from src.ui.widgets.statusbar import StatusBar
from src.ui.widgets.toolbar import ToolBar from src.ui.widgets.toolbar import ToolBar
from src.api.serverproxy_manager import ServerProxyManager from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost
from src.utilities.misc_helper import launch_url, iso_datestring_to_formatted_datestring from src.utilities.misc_helper import launch_url
from src.utilities.status_utils import RenderStatus
from src.utilities.zeroconf_server import ZeroconfServer
from src.version import APP_NAME from src.version import APP_NAME
logger = logging.getLogger() logger = logging.getLogger()
@@ -307,14 +307,15 @@ class MainWindow(QMainWindow):
get_time_elapsed(start_time, end_time) get_time_elapsed(start_time, end_time)
name = job.get('name') or os.path.basename(job.get('input_path', '')) name = job.get('name') or os.path.basename(job.get('input_path', ''))
engine_name = f"{job.get('renderer', '')}-{job.get('renderer_version')}" engine_name = f"{job.get('engine', '')}-{job.get('engine_version')}"
priority = str(job.get('priority', '')) priority = str(job.get('priority', ''))
total_frames = str(job.get('total_frames', '')) total_frames = str(job.get('total_frames', ''))
date_created_string = iso_datestring_to_formatted_datestring(job['date_created']) converted_time = datetime.datetime.fromisoformat(job['date_created'])
humanized_time = humanize.naturaltime(converted_time)
items = [QTableWidgetItem(job['id']), QTableWidgetItem(name), QTableWidgetItem(engine_name), items = [QTableWidgetItem(job['id']), QTableWidgetItem(name), QTableWidgetItem(engine_name),
QTableWidgetItem(priority), QTableWidgetItem(display_status), QTableWidgetItem(time_elapsed), QTableWidgetItem(priority), QTableWidgetItem(display_status), QTableWidgetItem(time_elapsed),
QTableWidgetItem(total_frames), QTableWidgetItem(date_created_string)] QTableWidgetItem(total_frames), QTableWidgetItem(humanized_time)]
for col, item in enumerate(items): for col, item in enumerate(items):
self.job_list_view.setItem(row, col, item) self.job_list_view.setItem(row, col, item)
+53 -14
View File
@@ -249,20 +249,6 @@ def num_to_alphanumeric(num):
return result[::-1] # Reverse the result to get the correct alphanumeric string return result[::-1] # Reverse the result to get the correct alphanumeric string
def iso_datestring_to_formatted_datestring(iso_date_string):
from dateutil import parser
import pytz
# Parse the ISO date string into a datetime object and convert timezones
date = parser.isoparse(iso_date_string).astimezone(pytz.UTC)
local_timezone = datetime.now().astimezone().tzinfo
date_local = date.astimezone(local_timezone)
# Format the date to the desired readable yet sortable format with 12-hour time
formatted_date = date_local.strftime('%Y-%m-%d %I:%M %p')
return formatted_date
def get_gpu_info(): def get_gpu_info():
"""Cross-platform GPU information retrieval""" """Cross-platform GPU information retrieval"""
@@ -384,3 +370,56 @@ def get_gpu_info():
return get_windows_gpu_info() return get_windows_gpu_info()
else: # Assume Linux or other else: # Assume Linux or other
return get_linux_gpu_info() return get_linux_gpu_info()
COMMON_RESOLUTIONS = {
# SD
"SD_480p": (640, 480),
"NTSC_DVD": (720, 480),
"PAL_DVD": (720, 576),
# HD
"HD_720p": (1280, 720),
"HD_900p": (1600, 900),
"HD_1080p": (1920, 1080),
# Cinema / Film
"2K_DCI": (2048, 1080),
"4K_DCI": (4096, 2160),
# UHD / Consumer
"UHD_4K": (3840, 2160),
"UHD_5K": (5120, 2880),
"UHD_8K": (7680, 4320),
# Ultrawide / Aspect Variants
"UW_1080p": (2560, 1080),
"UW_1440p": (3440, 1440),
"UW_5K": (5120, 2160),
# Mobile / Social
"VERTICAL_1080x1920": (1080, 1920),
"SQUARE_1080": (1080, 1080),
# Classic / Legacy
"VGA": (640, 480),
"SVGA": (800, 600),
"XGA": (1024, 768),
"WXGA": (1280, 800),
}
COMMON_FRAME_RATES = {
"23.976 (NTSC Film)": 23.976,
"24 (Cinema)": 24.0,
"25 (PAL)": 25.0,
"29.97 (NTSC)": 29.97,
"30": 30.0,
"48 (HFR Film)": 48.0,
"50 (PAL HFR)": 50.0,
"59.94": 59.94,
"60": 60.0,
"72": 72.0,
"90 (VR)": 90.0,
"120": 120.0,
"144 (Gaming)": 144.0,
"240 (HFR)": 240.0,
}