mirror of
https://github.com/blw1138/Zordon.git
synced 2026-02-05 13:46:10 +00:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,4 +8,6 @@
|
|||||||
/.github/
|
/.github/
|
||||||
*.idea
|
*.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/venv/
|
||||||
|
.env
|
||||||
venv/
|
venv/
|
||||||
|
|||||||
120
add_job.py
Normal file
120
add_job.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from server import start_server
|
||||||
|
from src.api.serverproxy_manager import ServerProxyManager
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Zordon CLI tool for preparing/submitting a render job",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# Required arguments
|
||||||
|
parser.add_argument("scene_file", help="Path to the scene file (e.g., .blend, .max, .mp4)")
|
||||||
|
parser.add_argument("engine", help="Desired render engine", choices=['blender', 'ffmpeg'])
|
||||||
|
|
||||||
|
# Frame range
|
||||||
|
parser.add_argument("--start", type=int, default=1, help="Start frame")
|
||||||
|
parser.add_argument("--end", type=int, default=1, help="End frame")
|
||||||
|
|
||||||
|
# Job metadata
|
||||||
|
parser.add_argument("--name", default=None, help="Job name")
|
||||||
|
|
||||||
|
# Output
|
||||||
|
parser.add_argument("--output", default="", help="Output path/pattern (e.g., /renders/frame_####.exr)")
|
||||||
|
|
||||||
|
# Target OS and Engine Version
|
||||||
|
parser.add_argument(
|
||||||
|
"--os",
|
||||||
|
choices=["any", "windows", "linux", "macos"],
|
||||||
|
default="any",
|
||||||
|
help="Target operating system for render workers"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--engine-version",
|
||||||
|
default="latest",
|
||||||
|
help="Required renderer/engine version number (e.g., '4.2', '5.0')"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional flags
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Print job details without submitting")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Basic validation
|
||||||
|
if not os.path.exists(args.scene_file):
|
||||||
|
print(f"Error: Scene file '{args.scene_file}' not found!", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.start > args.end:
|
||||||
|
print("Error: Start frame cannot be greater than end frame!", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Calculate total frames
|
||||||
|
total_frames = len(range(args.start, args.end + 1))
|
||||||
|
job_name = args.name or os.path.basename(args.scene_file)
|
||||||
|
file_path = os.path.abspath(args.scene_file)
|
||||||
|
|
||||||
|
# Print job summary
|
||||||
|
print("Render Job Summary:")
|
||||||
|
print(f" Job Name : {job_name}")
|
||||||
|
print(f" Scene File : {file_path}")
|
||||||
|
print(f" Engine : {args.engine}")
|
||||||
|
print(f" Frames : {args.start}-{args.end} → {total_frames} frames")
|
||||||
|
print(f" Output Path : {args.output or '(default from scene)'}")
|
||||||
|
print(f" Target OS : {args.os}")
|
||||||
|
print(f" Engine Version : {args.engine_version}")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\nDry run complete (no submission performed).")
|
||||||
|
return
|
||||||
|
|
||||||
|
local_hostname = socket.gethostname()
|
||||||
|
local_hostname = local_hostname + (".local" if not local_hostname.endswith(".local") else "")
|
||||||
|
found_proxy = ServerProxyManager.get_proxy_for_hostname(local_hostname)
|
||||||
|
|
||||||
|
is_connected = found_proxy.check_connection()
|
||||||
|
if not is_connected:
|
||||||
|
local_server_thread = threading.Thread(target=start_server, args=[True], daemon=True)
|
||||||
|
local_server_thread.start()
|
||||||
|
while not is_connected:
|
||||||
|
# todo: add timeout
|
||||||
|
# is_connected = found_proxy.check_connection()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
new_job = {"name": job_name, "engine": args.engine}
|
||||||
|
response = found_proxy.post_job_to_server(file_path, [new_job])
|
||||||
|
if response and response.ok:
|
||||||
|
print(f"Uploaded to {found_proxy.hostname} successfully!")
|
||||||
|
running_job_data = response.json()[0]
|
||||||
|
job_id = running_job_data.get('id')
|
||||||
|
print(f"Job {job_id} Summary:")
|
||||||
|
print(f" Status : {running_job_data.get('status')}")
|
||||||
|
print(f" Engine : {running_job_data.get('engine')}-{running_job_data.get('engine_version')}")
|
||||||
|
|
||||||
|
print("\nWaiting for render to complete...")
|
||||||
|
percent_complete = 0.0
|
||||||
|
while percent_complete < 1.0:
|
||||||
|
# add checks for errors
|
||||||
|
time.sleep(1)
|
||||||
|
running_job_data = found_proxy.get_job_info(job_id)
|
||||||
|
percent_complete = running_job_data['percent_complete']
|
||||||
|
sys.stdout.write("\x1b[1A") # Move up 1
|
||||||
|
sys.stdout.write("\x1b[0J") # Clear from cursor to end of screen (optional)
|
||||||
|
print(f"Percent Complete: {percent_complete:.2%}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
print("Finished rendering successfully!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -18,7 +18,7 @@ datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
|||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['main.py'],
|
['client.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=binaries,
|
binaries=binaries,
|
||||||
datas=datas,
|
datas=datas,
|
||||||
|
|||||||
90
server.spec
Normal file
90
server.spec
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
# - get version from version file
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
from version import APP_NAME, APP_VERSION, APP_AUTHOR
|
||||||
|
|
||||||
|
APP_NAME = APP_NAME + " Server"
|
||||||
|
datas = [('resources', 'resources'), ('src/engines/blender/scripts/', 'src/engines/blender/scripts')]
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = ['zeroconf']
|
||||||
|
tmp_ret = collect_all('zeroconf')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['server.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=1, # fyi: optim level 2 breaks on windows
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
|
||||||
|
import pyinstaller_versionfile
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
version_file_path = os.path.join(tempfile.gettempdir(), 'versionfile.txt')
|
||||||
|
|
||||||
|
pyinstaller_versionfile.create_versionfile(
|
||||||
|
output_file=version_file_path,
|
||||||
|
version=APP_VERSION,
|
||||||
|
company_name=APP_AUTHOR,
|
||||||
|
file_description=APP_NAME,
|
||||||
|
internal_name=APP_NAME,
|
||||||
|
legal_copyright=f"© {APP_AUTHOR}",
|
||||||
|
original_filename=f"{APP_NAME}.exe",
|
||||||
|
product_name=APP_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name=APP_NAME,
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=True,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
version=version_file_path
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # linux / macOS
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name=APP_NAME,
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=True,
|
||||||
|
upx=True,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None
|
||||||
|
)
|
||||||
@@ -55,7 +55,7 @@ def handle_uploaded_project_files(request, jobs_list, upload_directory):
|
|||||||
# Prepare the local filepath
|
# Prepare the local filepath
|
||||||
cleaned_path_name = jobs_list[0].get('name', os.path.splitext(referred_name)[0]).replace(' ', '-')
|
cleaned_path_name = jobs_list[0].get('name', os.path.splitext(referred_name)[0]).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"), renderer, cleaned_path_name]))
|
[datetime.now().strftime("%Y.%m.%d_%H.%M.%S"), engine_name, cleaned_path_name]))
|
||||||
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)
|
||||||
@@ -133,7 +133,7 @@ def process_zipped_project(zip_path):
|
|||||||
|
|
||||||
logger.debug(f"Zip files: {project_files}")
|
logger.debug(f"Zip files: {project_files}")
|
||||||
|
|
||||||
# supported_exts = RenderWorkerFactory.class_for_name(renderer).engine.supported_extensions
|
# supported_exts = RenderWorkerFactory.class_for_name(engine).engine.supported_extensions
|
||||||
# if supported_exts:
|
# if supported_exts:
|
||||||
# project_files = [file for file in project_files if any(file.endswith(ext) for ext in supported_exts)]
|
# project_files = [file for file in project_files if any(file.endswith(ext) for ext in supported_exts)]
|
||||||
|
|
||||||
|
|||||||
@@ -340,8 +340,8 @@ def delete_job(job_id):
|
|||||||
# Engine Info and Management:
|
# Engine Info and Management:
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
@server.get('/api/renderer_info')
|
@server.get('/api/engine_info')
|
||||||
def renderer_info():
|
def engine_info():
|
||||||
response_type = request.args.get('response_type', 'standard')
|
response_type = request.args.get('response_type', 'standard')
|
||||||
if response_type not in ['full', 'standard']:
|
if response_type not in ['full', 'standard']:
|
||||||
raise ValueError(f"Invalid response_type: {response_type}")
|
raise ValueError(f"Invalid response_type: {response_type}")
|
||||||
@@ -379,19 +379,19 @@ def renderer_info():
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Error fetching details for {engine.name()} renderer: {e}')
|
logger.error(f"Error fetching details for engine '{engine.name()}': {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
renderer_data = {}
|
engine_data = {}
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
futures = {executor.submit(process_engine, engine): engine.name() for engine in EngineManager.supported_engines()}
|
futures = {executor.submit(process_engine, engine): engine.name() for engine in EngineManager.supported_engines()}
|
||||||
|
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
result = future.result()
|
result = future.result()
|
||||||
if result:
|
if result:
|
||||||
renderer_data.update(result)
|
engine_data.update(result)
|
||||||
|
|
||||||
return renderer_data
|
return engine_data
|
||||||
|
|
||||||
|
|
||||||
@server.get('/api/<engine_name>/is_available')
|
@server.get('/api/<engine_name>/is_available')
|
||||||
@@ -442,22 +442,22 @@ def delete_engine_download():
|
|||||||
(f"Error deleting {json_data.get('engine')} {json_data.get('version')}", 500)
|
(f"Error deleting {json_data.get('engine')} {json_data.get('version')}", 500)
|
||||||
|
|
||||||
|
|
||||||
@server.get('/api/renderer/<renderer>/args')
|
@server.get('/api/engine/<engine_name>/args')
|
||||||
def get_renderer_args(renderer):
|
def get_engine_args(engine_name):
|
||||||
try:
|
try:
|
||||||
renderer_engine_class = EngineManager.engine_with_name(renderer)
|
engine_class = EngineManager.engine_with_name(engine_name)
|
||||||
return renderer_engine_class().get_arguments()
|
return engine_class().get_arguments()
|
||||||
except LookupError:
|
except LookupError:
|
||||||
return f"Cannot find renderer '{renderer}'", 400
|
return f"Cannot find engine '{engine_name}'", 400
|
||||||
|
|
||||||
|
|
||||||
@server.get('/api/renderer/<renderer>/help')
|
@server.get('/api/engine/<engine_name>/help')
|
||||||
def get_renderer_help(renderer):
|
def get_engine_help(engine_name):
|
||||||
try:
|
try:
|
||||||
renderer_engine_class = EngineManager.engine_with_name(renderer)
|
engine_class = EngineManager.engine_with_name(engine_name)
|
||||||
return renderer_engine_class().get_help()
|
return engine_class().get_help()
|
||||||
except LookupError:
|
except LookupError:
|
||||||
return f"Cannot find renderer '{renderer}'", 400
|
return f"Cannot find engine '{engine_name}'", 400
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class DistributedJobManager:
|
|||||||
logger.debug(f"New job output path: {output_path}")
|
logger.debug(f"New job output path: {output_path}")
|
||||||
|
|
||||||
# create & configure jobs
|
# create & configure jobs
|
||||||
worker = EngineManager.create_worker(renderer=new_job_attributes['renderer'],
|
worker = EngineManager.create_worker(engine_name=new_job_attributes['engine'],
|
||||||
input_path=loaded_project_local_path,
|
input_path=loaded_project_local_path,
|
||||||
output_path=output_path,
|
output_path=output_path,
|
||||||
engine_version=new_job_attributes.get('engine_version'),
|
engine_version=new_job_attributes.get('engine_version'),
|
||||||
@@ -303,14 +303,14 @@ class DistributedJobManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
parent_worker (Worker): The parent job what we're creating the subjobs for.
|
parent_worker (Worker): The parent job what we're creating the subjobs for.
|
||||||
new_job_attributes (dict): Dict of desired attributes for new job (frame count, renderer, output path, etc)
|
new_job_attributes (dict): Dict of desired attributes for new job (frame count, engine, output path, etc)
|
||||||
project_path (str): The path to the project.
|
project_path (str): The path to the project.
|
||||||
system_os (str, optional): Required OS. Default is any.
|
system_os (str, optional): Required OS. Default is any.
|
||||||
specific_servers (list, optional): List of specific servers to split work between. Defaults to all found.
|
specific_servers (list, optional): List of specific servers to split work between. Defaults to all found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check availability
|
# Check availability
|
||||||
available_servers = specific_servers if specific_servers else cls.find_available_servers(parent_worker.renderer,
|
available_servers = specific_servers if specific_servers else cls.find_available_servers(parent_worker.engine_name,
|
||||||
system_os)
|
system_os)
|
||||||
# skip if theres no external servers found
|
# skip if theres no external servers found
|
||||||
external_servers = [x for x in available_servers if x['hostname'] != parent_worker.hostname]
|
external_servers = [x for x in available_servers if x['hostname'] != parent_worker.hostname]
|
||||||
@@ -354,7 +354,7 @@ class DistributedJobManager:
|
|||||||
subjob['parent'] = f"{parent_worker.id}@{parent_worker.hostname}"
|
subjob['parent'] = f"{parent_worker.id}@{parent_worker.hostname}"
|
||||||
subjob['start_frame'] = server_data['frame_range'][0]
|
subjob['start_frame'] = server_data['frame_range'][0]
|
||||||
subjob['end_frame'] = server_data['frame_range'][-1]
|
subjob['end_frame'] = server_data['frame_range'][-1]
|
||||||
subjob['engine_version'] = parent_worker.renderer_version
|
subjob['engine_version'] = parent_worker.engine_version
|
||||||
logger.debug(f"Posting subjob with frames {subjob['start_frame']}-"
|
logger.debug(f"Posting subjob with frames {subjob['start_frame']}-"
|
||||||
f"{subjob['end_frame']} to {server_hostname}")
|
f"{subjob['end_frame']} to {server_hostname}")
|
||||||
post_results = RenderServerProxy(server_hostname).post_job_to_server(
|
post_results = RenderServerProxy(server_hostname).post_job_to_server(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class AERender(BaseRenderEngine):
|
|||||||
def version(self):
|
def version(self):
|
||||||
version = None
|
version = None
|
||||||
try:
|
try:
|
||||||
render_path = self.renderer_path()
|
render_path = self.engine_path()
|
||||||
if render_path:
|
if render_path:
|
||||||
ver_out = subprocess.check_output([render_path, '-version'], timeout=SUBPROCESS_TIMEOUT)
|
ver_out = subprocess.check_output([render_path, '-version'], timeout=SUBPROCESS_TIMEOUT)
|
||||||
version = ver_out.decode('utf-8').split(" ")[-1].strip()
|
version = ver_out.decode('utf-8').split(" ")[-1].strip()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Blender(BaseRenderEngine):
|
|||||||
def version(self):
|
def version(self):
|
||||||
version = None
|
version = None
|
||||||
try:
|
try:
|
||||||
render_path = self.renderer_path()
|
render_path = self.engine_path()
|
||||||
if render_path:
|
if render_path:
|
||||||
ver_out = subprocess.check_output([render_path, '-v'], timeout=SUBPROCESS_TIMEOUT,
|
ver_out = subprocess.check_output([render_path, '-v'], timeout=SUBPROCESS_TIMEOUT,
|
||||||
creationflags=_creationflags)
|
creationflags=_creationflags)
|
||||||
@@ -52,7 +52,7 @@ class Blender(BaseRenderEngine):
|
|||||||
def run_python_expression(self, project_path, python_expression, timeout=None):
|
def run_python_expression(self, project_path, python_expression, timeout=None):
|
||||||
if os.path.exists(project_path):
|
if os.path.exists(project_path):
|
||||||
try:
|
try:
|
||||||
return subprocess.run([self.renderer_path(), '-b', project_path, '--python-expr', python_expression],
|
return subprocess.run([self.engine_path(), '-b', project_path, '--python-expr', python_expression],
|
||||||
capture_output=True, timeout=timeout, creationflags=_creationflags)
|
capture_output=True, timeout=timeout, creationflags=_creationflags)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = f"Error running python expression in blender: {e}"
|
err_msg = f"Error running python expression in blender: {e}"
|
||||||
@@ -69,7 +69,7 @@ class Blender(BaseRenderEngine):
|
|||||||
raise FileNotFoundError(f'Python script not found: {script_path}')
|
raise FileNotFoundError(f'Python script not found: {script_path}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
command = [self.renderer_path(), '-b', '--python', script_path]
|
command = [self.engine_path(), '-b', '--python', script_path]
|
||||||
if project_path:
|
if project_path:
|
||||||
command.insert(2, project_path)
|
command.insert(2, project_path)
|
||||||
result = subprocess.run(command, capture_output=True, timeout=timeout, creationflags=_creationflags)
|
result = subprocess.run(command, capture_output=True, timeout=timeout, creationflags=_creationflags)
|
||||||
@@ -132,7 +132,7 @@ class Blender(BaseRenderEngine):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_arguments(self):
|
def get_arguments(self):
|
||||||
help_text = subprocess.check_output([self.renderer_path(), '-h'], creationflags=_creationflags).decode('utf-8')
|
help_text = subprocess.check_output([self.engine_path(), '-h'], creationflags=_creationflags).decode('utf-8')
|
||||||
lines = help_text.splitlines()
|
lines = help_text.splitlines()
|
||||||
|
|
||||||
options = {}
|
options = {}
|
||||||
@@ -179,7 +179,7 @@ class Blender(BaseRenderEngine):
|
|||||||
logger.error("GPU data not found in the output.")
|
logger.error("GPU data not found in the output.")
|
||||||
|
|
||||||
def supported_render_engines(self):
|
def supported_render_engines(self):
|
||||||
engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
|
engine_output = subprocess.run([self.engine_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
|
||||||
capture_output=True, creationflags=_creationflags).stdout.decode('utf-8').strip()
|
capture_output=True, creationflags=_creationflags).stdout.decode('utf-8').strip()
|
||||||
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
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
|
|
||||||
def generate_worker_subprocess(self):
|
def generate_worker_subprocess(self):
|
||||||
|
|
||||||
cmd = [self.renderer_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')
|
||||||
cmd.append(self.input_path)
|
cmd.append(self.input_path)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SUBPROCESS_TIMEOUT = 5
|
|||||||
|
|
||||||
class BaseRenderEngine(object):
|
class BaseRenderEngine(object):
|
||||||
"""Base class for render engines. This class provides common functionality and structure for various rendering
|
"""Base class for render engines. This class provides common functionality and structure for various rendering
|
||||||
engines. Create subclasses and override the methods marked below to add additional renderers
|
engines. Create subclasses and override the methods marked below to add additional engines
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
install_paths (list): A list of default installation paths where the render engine
|
install_paths (list): A list of default installation paths where the render engine
|
||||||
@@ -24,13 +24,13 @@ class BaseRenderEngine(object):
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
def __init__(self, custom_path=None):
|
def __init__(self, custom_path=None):
|
||||||
self.custom_renderer_path = custom_path
|
self.custom_engine_path = custom_path
|
||||||
if not self.renderer_path() or not os.path.exists(self.renderer_path()):
|
if not self.engine_path() or not os.path.exists(self.engine_path()):
|
||||||
raise FileNotFoundError(f"Cannot find path to renderer for {self.name()} instance: {self.renderer_path()}")
|
raise FileNotFoundError(f"Cannot find path to engine for {self.name()} instance: {self.engine_path()}")
|
||||||
|
|
||||||
if not os.access(self.renderer_path(), os.X_OK):
|
if not os.access(self.engine_path(), os.X_OK):
|
||||||
logger.warning(f"Path is not executable. Setting permissions to 755 for {self.renderer_path()}")
|
logger.warning(f"Path is not executable. Setting permissions to 755 for {self.engine_path()}")
|
||||||
os.chmod(self.renderer_path(), 0o755)
|
os.chmod(self.engine_path(), 0o755)
|
||||||
|
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return the version number as a string.
|
"""Return the version number as a string.
|
||||||
@@ -60,7 +60,7 @@ class BaseRenderEngine(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_output_formats(cls):
|
def get_output_formats(cls):
|
||||||
"""Returns a list of available output formats supported by the renderer.
|
"""Returns a list of available output formats supported by the engine.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[str]: A list of strings representing the available output formats.
|
list[str]: A list of strings representing the available output formats.
|
||||||
@@ -83,20 +83,20 @@ class BaseRenderEngine(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_help(self):
|
def get_help(self):
|
||||||
"""Retrieves the help documentation for the renderer.
|
"""Retrieves the help documentation for the engine.
|
||||||
|
|
||||||
This method runs the renderer's help command (default: '-h') and captures the output.
|
This method runs the engine's help command (default: '-h') and captures the output.
|
||||||
Override this method if the renderer uses a different help flag.
|
Override this method if the engine uses a different help flag.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The help documentation as a string.
|
str: The help documentation as a string.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FileNotFoundError: If the renderer path is not found.
|
FileNotFoundError: If the engine path is not found.
|
||||||
"""
|
"""
|
||||||
path = self.renderer_path()
|
path = self.engine_path()
|
||||||
if not path:
|
if not path:
|
||||||
raise FileNotFoundError("renderer path not found")
|
raise FileNotFoundError(f"Engine path not found: {path}")
|
||||||
creationflags = subprocess.CREATE_NO_WINDOW if platform.system() == 'Windows' else 0
|
creationflags = subprocess.CREATE_NO_WINDOW if platform.system() == 'Windows' else 0
|
||||||
help_doc = subprocess.check_output([path, '-h'], stderr=subprocess.STDOUT,
|
help_doc = subprocess.check_output([path, '-h'], stderr=subprocess.STDOUT,
|
||||||
timeout=SUBPROCESS_TIMEOUT, creationflags=creationflags).decode('utf-8')
|
timeout=SUBPROCESS_TIMEOUT, creationflags=creationflags).decode('utf-8')
|
||||||
@@ -141,15 +141,15 @@ class BaseRenderEngine(object):
|
|||||||
# Do Not Override These Methods:
|
# Do Not Override These Methods:
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
def renderer_path(self):
|
def engine_path(self):
|
||||||
return self.custom_renderer_path or self.default_renderer_path()
|
return self.custom_engine_path or self.default_engine_path()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def name(cls):
|
def name(cls):
|
||||||
return str(cls.__name__).lower()
|
return str(cls.__name__).lower()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_renderer_path(cls):
|
def default_engine_path(cls):
|
||||||
path = None
|
path = None
|
||||||
try: # Linux and macOS
|
try: # Linux and macOS
|
||||||
path = subprocess.check_output(['which', cls.name()], timeout=SUBPROCESS_TIMEOUT).decode('utf-8').strip()
|
path = subprocess.check_output(['which', cls.name()], timeout=SUBPROCESS_TIMEOUT).decode('utf-8').strip()
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ class BaseRenderWorker(Base):
|
|||||||
date_created = Column(DateTime)
|
date_created = Column(DateTime)
|
||||||
start_time = Column(DateTime, nullable=True)
|
start_time = Column(DateTime, nullable=True)
|
||||||
end_time = Column(DateTime, nullable=True)
|
end_time = Column(DateTime, nullable=True)
|
||||||
renderer = Column(String)
|
engine_name = Column(String)
|
||||||
renderer_version = Column(String)
|
engine_version = Column(String)
|
||||||
renderer_path = Column(String)
|
engine_path = Column(String)
|
||||||
priority = Column(Integer)
|
priority = Column(Integer)
|
||||||
project_length = Column(Integer)
|
project_length = Column(Integer)
|
||||||
start_frame = Column(Integer)
|
start_frame = Column(Integer)
|
||||||
@@ -46,8 +46,6 @@ class BaseRenderWorker(Base):
|
|||||||
file_hash = Column(String)
|
file_hash = Column(String)
|
||||||
_status = Column(String)
|
_status = Column(String)
|
||||||
|
|
||||||
engine = None
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# Required Overrides for Subclasses:
|
# Required Overrides for Subclasses:
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
@@ -57,7 +55,7 @@ class BaseRenderWorker(Base):
|
|||||||
|
|
||||||
if not ignore_extensions:
|
if not ignore_extensions:
|
||||||
if not any(ext in input_path for ext in self.engine.supported_extensions()):
|
if not any(ext in input_path for ext in self.engine.supported_extensions()):
|
||||||
err_meg = f'Cannot find valid project with supported file extension for {self.engine.name()} renderer'
|
err_meg = f"Cannot find valid project with supported file extension for '{self.engine.name()}'"
|
||||||
logger.error(err_meg)
|
logger.error(err_meg)
|
||||||
raise ValueError(err_meg)
|
raise ValueError(err_meg)
|
||||||
if not self.engine:
|
if not self.engine:
|
||||||
@@ -74,10 +72,10 @@ class BaseRenderWorker(Base):
|
|||||||
self.output_path = output_path
|
self.output_path = output_path
|
||||||
self.args = args or {}
|
self.args = args or {}
|
||||||
self.date_created = datetime.now()
|
self.date_created = datetime.now()
|
||||||
self.renderer = self.engine.name()
|
self.engine_name = self.engine.name()
|
||||||
self.renderer_path = engine_path
|
self.engine_path = engine_path
|
||||||
self.renderer_version = self.engine(engine_path).version()
|
self.engine_version = self.engine(engine_path).version()
|
||||||
self.custom_renderer_path = None
|
self.custom_engine_path = None
|
||||||
self.priority = priority
|
self.priority = priority
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.children = {}
|
self.children = {}
|
||||||
@@ -116,17 +114,17 @@ class BaseRenderWorker(Base):
|
|||||||
raise NotImplementedError("generate_worker_subprocess not implemented")
|
raise NotImplementedError("generate_worker_subprocess not implemented")
|
||||||
|
|
||||||
def _parse_stdout(self, line):
|
def _parse_stdout(self, line):
|
||||||
"""Parses a line of standard output from the renderer.
|
"""Parses a line of standard output from the engine.
|
||||||
|
|
||||||
This method should be overridden in a subclass to implement the logic for processing
|
This method should be overridden in a subclass to implement the logic for processing
|
||||||
and interpreting a single line of output from the renderer's standard output stream.
|
and interpreting a single line of output from the engine's standard output stream.
|
||||||
|
|
||||||
On frame completion, the subclass should:
|
On frame completion, the subclass should:
|
||||||
1. Update value of self.current_frame
|
1. Update value of self.current_frame
|
||||||
2. Call self._send_frame_complete_notification()
|
2. Call self._send_frame_complete_notification()
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
line (str): A line of text from the renderer's standard output.
|
line (str): A line of text from the engine's standard output.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: If the method is not overridden in a subclass.
|
NotImplementedError: If the method is not overridden in a subclass.
|
||||||
@@ -152,7 +150,7 @@ class BaseRenderWorker(Base):
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Job id:{self.id} p{self.priority} {self.renderer}-{self.renderer_version} '{self.name}' status:{self.status.value}>"
|
return f"<Job id:{self.id} p{self.priority} {self.engine_name}-{self.engine_version} '{self.name}' status:{self.status.value}>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_frames(self):
|
def total_frames(self):
|
||||||
@@ -215,7 +213,7 @@ class BaseRenderWorker(Base):
|
|||||||
self.errors.append(msg)
|
self.errors.append(msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.exists(self.renderer_path):
|
if not os.path.exists(self.engine_path):
|
||||||
self.status = RenderStatus.ERROR
|
self.status = RenderStatus.ERROR
|
||||||
msg = f'Cannot find render engine path for {self.engine.name()}'
|
msg = f'Cannot find render engine path for {self.engine.name()}'
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
@@ -269,14 +267,14 @@ class BaseRenderWorker(Base):
|
|||||||
logger.error(err_msg)
|
logger.error(err_msg)
|
||||||
self.errors.append(err_msg)
|
self.errors.append(err_msg)
|
||||||
|
|
||||||
# handle instances where renderer exits ok but doesnt generate files
|
# handle instances where engine exits ok but doesnt generate files
|
||||||
if not return_code and not file_count_has_increased:
|
if not return_code and not file_count_has_increased:
|
||||||
err_msg = (f"{self.engine.name()} render exited ok, but file count has not increased. "
|
err_msg = (f"{self.engine.name()} render exited ok, but file count has not increased. "
|
||||||
f"Count is still {len(self.file_list())}")
|
f"Count is still {len(self.file_list())}")
|
||||||
log_file.write(f'Error: {err_msg}\n\n')
|
log_file.write(f'Error: {err_msg}\n\n')
|
||||||
self.errors.append(err_msg)
|
self.errors.append(err_msg)
|
||||||
|
|
||||||
# only count the attempt as failed if renderer creates no output - reset counter on successful output
|
# only count the attempt as failed if engine creates no output - reset counter on successful output
|
||||||
failed_attempts = 0 if file_count_has_increased else failed_attempts + 1
|
failed_attempts = 0 if file_count_has_increased else failed_attempts + 1
|
||||||
|
|
||||||
def __run__wait_for_subjobs(self, logfile):
|
def __run__wait_for_subjobs(self, logfile):
|
||||||
@@ -302,7 +300,7 @@ class BaseRenderWorker(Base):
|
|||||||
with open(self.log_path(), "a") as log_file:
|
with open(self.log_path(), "a") as log_file:
|
||||||
|
|
||||||
self.log_and_print(f"{self.start_time.isoformat()} - Starting "
|
self.log_and_print(f"{self.start_time.isoformat()} - Starting "
|
||||||
f"{self.engine.name()} {self.renderer_version} render job for {self.name} "
|
f"{self.engine.name()} {self.engine_version} render job for {self.name} "
|
||||||
f"({self.input_path})", log_file)
|
f"({self.input_path})", log_file)
|
||||||
log_file.write(f"\n")
|
log_file.write(f"\n")
|
||||||
if not self.children:
|
if not self.children:
|
||||||
@@ -493,8 +491,8 @@ class BaseRenderWorker(Base):
|
|||||||
'file_hash': self.file_hash,
|
'file_hash': self.file_hash,
|
||||||
'percent_complete': self.percent_complete(),
|
'percent_complete': self.percent_complete(),
|
||||||
'file_count': len(self.file_list()),
|
'file_count': len(self.file_list()),
|
||||||
'renderer': self.renderer,
|
'engine': self.engine_name,
|
||||||
'renderer_version': self.renderer_version,
|
'engine_version': self.engine_version,
|
||||||
'errors': getattr(self, 'errors', None),
|
'errors': getattr(self, 'errors', None),
|
||||||
'start_frame': self.start_frame,
|
'start_frame': self.start_frame,
|
||||||
'end_frame': self.end_frame,
|
'end_frame': self.end_frame,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ logger = logging.getLogger()
|
|||||||
|
|
||||||
|
|
||||||
class EngineManager:
|
class EngineManager:
|
||||||
"""Class that manages different versions of installed renderers 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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class EngineManager:
|
|||||||
'version': version or 'error',
|
'version': version or 'error',
|
||||||
'system_os': current_system_os(),
|
'system_os': current_system_os(),
|
||||||
'cpu': current_system_cpu(),
|
'cpu': current_system_cpu(),
|
||||||
'path': eng.default_renderer_path(),
|
'path': eng.default_engine_path(),
|
||||||
'type': 'system'
|
'type': 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class EngineManager:
|
|||||||
futures = {
|
futures = {
|
||||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||||
for eng in cls.supported_engines()
|
for eng in cls.supported_engines()
|
||||||
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
if eng.default_engine_path() and (not filter_name or filter_name == eng.name())
|
||||||
}
|
}
|
||||||
|
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
@@ -240,14 +240,14 @@ class EngineManager:
|
|||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_worker(cls, renderer, 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):
|
||||||
|
|
||||||
worker_class = cls.engine_with_name(renderer).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(renderer)
|
all_versions = cls.all_versions_for_engine(engine_name)
|
||||||
if not all_versions:
|
if not all_versions:
|
||||||
raise FileNotFoundError(f"Cannot find any installed {renderer} engines")
|
raise FileNotFoundError(f"Cannot find any installed '{engine_name}' engines")
|
||||||
|
|
||||||
# Find the path to the requested engine version or use default
|
# Find the path to the requested engine version or use default
|
||||||
engine_path = None
|
engine_path = None
|
||||||
@@ -259,9 +259,9 @@ class EngineManager:
|
|||||||
|
|
||||||
# Download the required engine if not found locally
|
# Download the required engine if not found locally
|
||||||
if not engine_path:
|
if not engine_path:
|
||||||
download_result = cls.download_engine(renderer, engine_version)
|
download_result = cls.download_engine(engine_name, engine_version)
|
||||||
if not download_result:
|
if not download_result:
|
||||||
raise FileNotFoundError(f"Cannot download requested version: {renderer} {engine_version}")
|
raise FileNotFoundError(f"Cannot download requested version: {engine_name} {engine_version}")
|
||||||
engine_path = download_result['path']
|
engine_path = download_result['path']
|
||||||
logger.info("Engine downloaded. Creating worker.")
|
logger.info("Engine downloaded. Creating worker.")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
return FFMPEGUI.get_options(self)
|
return FFMPEGUI.get_options(self)
|
||||||
|
|
||||||
def supported_extensions(self):
|
def supported_extensions(self):
|
||||||
help_text = (subprocess.check_output([self.renderer_path(), '-h', 'full'], stderr=subprocess.STDOUT,
|
help_text = (subprocess.check_output([self.engine_path(), '-h', 'full'], stderr=subprocess.STDOUT,
|
||||||
creationflags=_creationflags).decode('utf-8'))
|
creationflags=_creationflags).decode('utf-8'))
|
||||||
found = re.findall(r'extensions that .* is allowed to access \(default "(.*)"', help_text)
|
found = re.findall(r'extensions that .* is allowed to access \(default "(.*)"', help_text)
|
||||||
found_extensions = set()
|
found_extensions = set()
|
||||||
@@ -35,7 +35,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
def version(self):
|
def version(self):
|
||||||
version = None
|
version = None
|
||||||
try:
|
try:
|
||||||
ver_out = subprocess.check_output([self.renderer_path(), '-version'], timeout=SUBPROCESS_TIMEOUT,
|
ver_out = subprocess.check_output([self.engine_path(), '-version'], timeout=SUBPROCESS_TIMEOUT,
|
||||||
creationflags=_creationflags).decode('utf-8')
|
creationflags=_creationflags).decode('utf-8')
|
||||||
match = re.match(r".*version\s*([\w.*]+)\W*", ver_out)
|
match = re.match(r".*version\s*([\w.*]+)\W*", ver_out)
|
||||||
if match:
|
if match:
|
||||||
@@ -82,7 +82,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
return None
|
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.engine_path(), '-encoders'], stderr=subprocess.DEVNULL,
|
||||||
timeout=SUBPROCESS_TIMEOUT, creationflags=_creationflags).decode('utf-8')
|
timeout=SUBPROCESS_TIMEOUT, creationflags=_creationflags).decode('utf-8')
|
||||||
pattern = r'(?P<type>[VASFXBD.]{6})\s+(?P<name>\S{2,})\s+(?P<description>.*)'
|
pattern = r'(?P<type>[VASFXBD.]{6})\s+(?P<name>\S{2,})\s+(?P<description>.*)'
|
||||||
encoders = [m.groupdict() for m in re.finditer(pattern, raw_stdout)]
|
encoders = [m.groupdict() for m in re.finditer(pattern, raw_stdout)]
|
||||||
@@ -94,7 +94,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
|
|
||||||
def get_all_formats(self):
|
def get_all_formats(self):
|
||||||
try:
|
try:
|
||||||
formats_raw = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL,
|
formats_raw = subprocess.check_output([self.engine_path(), '-formats'], stderr=subprocess.DEVNULL,
|
||||||
timeout=SUBPROCESS_TIMEOUT,
|
timeout=SUBPROCESS_TIMEOUT,
|
||||||
creationflags=_creationflags).decode('utf-8')
|
creationflags=_creationflags).decode('utf-8')
|
||||||
pattern = r'(?P<type>[DE]{1,2})\s+(?P<id>\S{2,})\s+(?P<name>.*)'
|
pattern = r'(?P<type>[DE]{1,2})\s+(?P<id>\S{2,})\s+(?P<name>.*)'
|
||||||
@@ -108,7 +108,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
# Extract the common extension using regex
|
# Extract the common extension using regex
|
||||||
muxer_flag = 'muxer' if 'E' in ffmpeg_format['type'] else 'demuxer'
|
muxer_flag = 'muxer' if 'E' in ffmpeg_format['type'] else 'demuxer'
|
||||||
format_detail_raw = subprocess.check_output(
|
format_detail_raw = subprocess.check_output(
|
||||||
[self.renderer_path(), '-hide_banner', '-h', f"{muxer_flag}={ffmpeg_format['id']}"],
|
[self.engine_path(), '-hide_banner', '-h', f"{muxer_flag}={ffmpeg_format['id']}"],
|
||||||
creationflags=_creationflags).decode('utf-8')
|
creationflags=_creationflags).decode('utf-8')
|
||||||
pattern = r"Common extensions: (\w+)"
|
pattern = r"Common extensions: (\w+)"
|
||||||
common_extensions = re.findall(pattern, format_detail_raw)
|
common_extensions = re.findall(pattern, format_detail_raw)
|
||||||
@@ -121,7 +121,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
return [x['id'] for x in self.get_all_formats() if 'E' in x['type'].upper()]
|
return [x['id'] for x in self.get_all_formats() if 'E' in x['type'].upper()]
|
||||||
|
|
||||||
def get_frame_count(self, path_to_file):
|
def get_frame_count(self, path_to_file):
|
||||||
raw_stdout = subprocess.check_output([self.renderer_path(), '-i', path_to_file, '-map', '0:v:0', '-c', 'copy',
|
raw_stdout = subprocess.check_output([self.engine_path(), '-i', path_to_file, '-map', '0:v:0', '-c', 'copy',
|
||||||
'-f', 'null', '-'], stderr=subprocess.STDOUT,
|
'-f', 'null', '-'], stderr=subprocess.STDOUT,
|
||||||
timeout=SUBPROCESS_TIMEOUT, creationflags=_creationflags).decode('utf-8')
|
timeout=SUBPROCESS_TIMEOUT, creationflags=_creationflags).decode('utf-8')
|
||||||
match = re.findall(r'frame=\s*(\d+)', raw_stdout)
|
match = re.findall(r'frame=\s*(\d+)', raw_stdout)
|
||||||
@@ -131,7 +131,7 @@ class FFMPEG(BaseRenderEngine):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
def get_arguments(self):
|
def get_arguments(self):
|
||||||
help_text = (subprocess.check_output([self.renderer_path(), '-h', 'long'], stderr=subprocess.STDOUT,
|
help_text = (subprocess.check_output([self.engine_path(), '-h', 'long'], stderr=subprocess.STDOUT,
|
||||||
creationflags=_creationflags).decode('utf-8'))
|
creationflags=_creationflags).decode('utf-8'))
|
||||||
lines = help_text.splitlines()
|
lines = help_text.splitlines()
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class FFMPEGRenderWorker(BaseRenderWorker):
|
|||||||
|
|
||||||
def generate_worker_subprocess(self):
|
def generate_worker_subprocess(self):
|
||||||
|
|
||||||
cmd = [self.renderer_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('x_resolution', None) and self.args.get('y_resolution', None):
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class RenderQueue:
|
|||||||
try:
|
try:
|
||||||
not_started = cls.jobs_with_status(RenderStatus.NOT_STARTED, priority_sorted=True)
|
not_started = cls.jobs_with_status(RenderStatus.NOT_STARTED, priority_sorted=True)
|
||||||
for job in not_started:
|
for job in not_started:
|
||||||
if cls.is_available_for_job(job.renderer, job.priority):
|
if cls.is_available_for_job(job.engine_name, job.priority):
|
||||||
cls.start_job(job)
|
cls.start_job(job)
|
||||||
|
|
||||||
scheduled = cls.jobs_with_status(RenderStatus.SCHEDULED, priority_sorted=True)
|
scheduled = cls.jobs_with_status(RenderStatus.SCHEDULED, priority_sorted=True)
|
||||||
@@ -145,7 +145,7 @@ class RenderQueue:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def renderer_instances(cls):
|
def renderer_instances(cls):
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
all_instances = [x.renderer for x in cls.running_jobs()]
|
all_instances = [x.engine_name for x in cls.running_jobs()]
|
||||||
return Counter(all_instances)
|
return Counter(all_instances)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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_viewer import EngineHelpViewer
|
from src.ui.engine_help_window import EngineHelpViewer
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.notes_group = None
|
self.notes_group = None
|
||||||
self.frame_rate_input = None
|
self.frame_rate_input = None
|
||||||
self.resolution_x_input = None
|
self.resolution_x_input = None
|
||||||
self.renderer_group = None
|
self.engine_group = None
|
||||||
self.output_settings_group = None
|
self.output_settings_group = None
|
||||||
self.resolution_y_input = None
|
self.resolution_y_input = None
|
||||||
self.project_path = project_path
|
self.project_path = project_path
|
||||||
@@ -34,17 +34,17 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.load_file_group = None
|
self.load_file_group = None
|
||||||
self.current_engine_options = None
|
self.current_engine_options = None
|
||||||
self.file_format_combo = None
|
self.file_format_combo = None
|
||||||
self.renderer_options_layout = None
|
self.engine_options_layout = None
|
||||||
self.cameras_list = None
|
self.cameras_list = None
|
||||||
self.cameras_group = None
|
self.cameras_group = None
|
||||||
self.renderer_version_combo = None
|
self.engine_version_combo = None
|
||||||
self.worker_thread = None
|
self.worker_thread = None
|
||||||
self.msg_box = 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
|
||||||
self.submit_progress = None
|
self.submit_progress = None
|
||||||
self.renderer_type = None
|
self.engine_type = None
|
||||||
self.process_label = None
|
self.process_label = None
|
||||||
self.process_progress_bar = None
|
self.process_progress_bar = None
|
||||||
self.splitjobs_same_os = None
|
self.splitjobs_same_os = None
|
||||||
@@ -62,13 +62,13 @@ class NewRenderJobForm(QWidget):
|
|||||||
|
|
||||||
# Job / Server Data
|
# Job / Server Data
|
||||||
self.server_proxy = RenderServerProxy(socket.gethostname())
|
self.server_proxy = RenderServerProxy(socket.gethostname())
|
||||||
self.renderer_info = None
|
self.engine_info = None
|
||||||
self.project_info = None
|
self.project_info = None
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
self.setWindowTitle("New Job")
|
self.setWindowTitle("New Job")
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.update_renderer_info()
|
self.update_engine_info()
|
||||||
self.setup_project()
|
self.setup_project()
|
||||||
|
|
||||||
# get renderer info in bg thread
|
# get renderer info in bg thread
|
||||||
@@ -182,33 +182,33 @@ class NewRenderJobForm(QWidget):
|
|||||||
# add group to layout
|
# add group to layout
|
||||||
main_layout.addWidget(self.output_settings_group)
|
main_layout.addWidget(self.output_settings_group)
|
||||||
|
|
||||||
# Renderer Group
|
# Engine Group
|
||||||
self.renderer_group = QGroupBox("Renderer Settings")
|
self.engine_group = QGroupBox("Engine Settings")
|
||||||
renderer_group_layout = QVBoxLayout(self.renderer_group)
|
engine_group_layout = QVBoxLayout(self.engine_group)
|
||||||
renderer_layout = QHBoxLayout()
|
engine_layout = QHBoxLayout()
|
||||||
renderer_layout.addWidget(QLabel("Renderer:"))
|
engine_layout.addWidget(QLabel("Engine:"))
|
||||||
self.renderer_type = QComboBox()
|
self.engine_type = QComboBox()
|
||||||
self.renderer_type.currentIndexChanged.connect(self.renderer_changed)
|
self.engine_type.currentIndexChanged.connect(self.engine_changed)
|
||||||
renderer_layout.addWidget(self.renderer_type)
|
engine_layout.addWidget(self.engine_type)
|
||||||
# Version
|
# Version
|
||||||
renderer_layout.addWidget(QLabel("Version:"))
|
engine_layout.addWidget(QLabel("Version:"))
|
||||||
self.renderer_version_combo = QComboBox()
|
self.engine_version_combo = QComboBox()
|
||||||
self.renderer_version_combo.addItem('latest')
|
self.engine_version_combo.addItem('latest')
|
||||||
renderer_layout.addWidget(self.renderer_version_combo)
|
engine_layout.addWidget(self.engine_version_combo)
|
||||||
renderer_group_layout.addLayout(renderer_layout)
|
engine_group_layout.addLayout(engine_layout)
|
||||||
# dynamic options
|
# dynamic options
|
||||||
self.renderer_options_layout = QVBoxLayout()
|
self.engine_options_layout = QVBoxLayout()
|
||||||
renderer_group_layout.addLayout(self.renderer_options_layout)
|
engine_group_layout.addLayout(self.engine_options_layout)
|
||||||
# Raw Args
|
# Raw Args
|
||||||
raw_args_layout = QHBoxLayout(self.renderer_group)
|
raw_args_layout = QHBoxLayout(self.engine_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_group_layout.addLayout(raw_args_layout)
|
engine_group_layout.addLayout(raw_args_layout)
|
||||||
main_layout.addWidget(self.renderer_group)
|
main_layout.addWidget(self.engine_group)
|
||||||
|
|
||||||
# Cameras Group
|
# Cameras Group
|
||||||
self.cameras_group = QGroupBox("Cameras")
|
self.cameras_group = QGroupBox("Cameras")
|
||||||
@@ -240,28 +240,28 @@ 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)
|
||||||
|
|
||||||
self.toggle_renderer_enablement(False)
|
self.toggle_engine_enablement(False)
|
||||||
|
|
||||||
def update_renderer_info(self):
|
def update_engine_info(self):
|
||||||
# get the renderer info and add them all to the ui
|
# get the engine info and add them all to the ui
|
||||||
self.renderer_info = self.server_proxy.get_renderer_info(response_type='full')
|
self.engine_info = self.server_proxy.get_engine_info(response_type='full')
|
||||||
self.renderer_type.addItems(self.renderer_info.keys())
|
self.engine_type.addItems(self.engine_info.keys())
|
||||||
# select the best renderer for the file type
|
# select the best engine for the file type
|
||||||
engine = EngineManager.engine_for_project_path(self.project_path)
|
engine = EngineManager.engine_for_project_path(self.project_path)
|
||||||
self.renderer_type.setCurrentText(engine.name().lower())
|
self.engine_type.setCurrentText(engine.name().lower())
|
||||||
# refresh ui
|
# refresh ui
|
||||||
self.renderer_changed()
|
self.engine_changed()
|
||||||
|
|
||||||
def renderer_changed(self):
|
def engine_changed(self):
|
||||||
# load the version numbers
|
# load the version numbers
|
||||||
current_renderer = self.renderer_type.currentText().lower() or self.renderer_type.itemText(0)
|
current_engine = self.engine_type.currentText().lower() or self.engine_type.itemText(0)
|
||||||
self.renderer_version_combo.clear()
|
self.engine_version_combo.clear()
|
||||||
self.renderer_version_combo.addItem('latest')
|
self.engine_version_combo.addItem('latest')
|
||||||
self.file_format_combo.clear()
|
self.file_format_combo.clear()
|
||||||
if current_renderer:
|
if current_engine:
|
||||||
renderer_vers = [version_info['version'] for version_info in self.renderer_info[current_renderer]['versions']]
|
engine_vers = [version_info['version'] for version_info in self.engine_info[current_engine]['versions']]
|
||||||
self.renderer_version_combo.addItems(renderer_vers)
|
self.engine_version_combo.addItems(engine_vers)
|
||||||
self.file_format_combo.addItems(self.renderer_info[current_renderer]['supported_export_formats'])
|
self.file_format_combo.addItems(self.engine_info[current_engine]['supported_export_formats'])
|
||||||
|
|
||||||
def update_server_list(self):
|
def update_server_list(self):
|
||||||
clients = ZeroconfServer.found_hostnames()
|
clients = ZeroconfServer.found_hostnames()
|
||||||
@@ -278,7 +278,7 @@ class NewRenderJobForm(QWidget):
|
|||||||
# UI stuff on main thread
|
# UI stuff on main thread
|
||||||
self.process_progress_bar.setHidden(False)
|
self.process_progress_bar.setHidden(False)
|
||||||
self.process_label.setHidden(False)
|
self.process_label.setHidden(False)
|
||||||
self.toggle_renderer_enablement(False)
|
self.toggle_engine_enablement(False)
|
||||||
|
|
||||||
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(' ', '_')
|
||||||
@@ -296,8 +296,8 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.render_name_input.setText(directory)
|
self.render_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/renderer/'
|
url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/engine/'
|
||||||
f'{self.renderer_type.currentText()}/help')
|
f'{self.engine_type.currentText()}/help')
|
||||||
self.engine_help_viewer = EngineHelpViewer(url)
|
self.engine_help_viewer = EngineHelpViewer(url)
|
||||||
self.engine_help_viewer.show()
|
self.engine_help_viewer.show()
|
||||||
|
|
||||||
@@ -306,20 +306,20 @@ class NewRenderJobForm(QWidget):
|
|||||||
def post_get_project_info_update(self):
|
def post_get_project_info_update(self):
|
||||||
"""Called by the GetProjectInfoWorker - Do not call directly."""
|
"""Called by the GetProjectInfoWorker - Do not call directly."""
|
||||||
try:
|
try:
|
||||||
# Set the best renderer we can find
|
# Set the best engine we can find
|
||||||
input_path = self.scene_file_input.text()
|
input_path = self.scene_file_input.text()
|
||||||
engine = EngineManager.engine_for_project_path(input_path)
|
engine = EngineManager.engine_for_project_path(input_path)
|
||||||
|
|
||||||
engine_index = self.renderer_type.findText(engine.name().lower())
|
engine_index = self.engine_type.findText(engine.name().lower())
|
||||||
if engine_index >= 0:
|
if engine_index >= 0:
|
||||||
self.renderer_type.setCurrentIndex(engine_index)
|
self.engine_type.setCurrentIndex(engine_index)
|
||||||
else:
|
else:
|
||||||
self.renderer_type.setCurrentIndex(0) #todo: find out why we don't have renderer info yet
|
self.engine_type.setCurrentIndex(0) #todo: find out why we don't have engine info yet
|
||||||
# not ideal but if we don't have the renderer info we have to pick something
|
# not ideal but if we don't have the engine info we have to pick something
|
||||||
|
|
||||||
# cleanup progress UI
|
# cleanup progress UI
|
||||||
self.load_file_group.setHidden(True)
|
self.load_file_group.setHidden(True)
|
||||||
self.toggle_renderer_enablement(True)
|
self.toggle_engine_enablement(True)
|
||||||
|
|
||||||
# Load scene data
|
# 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'))
|
||||||
@@ -347,9 +347,9 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.cameras_group.setHidden(True)
|
self.cameras_group.setHidden(True)
|
||||||
|
|
||||||
# Dynamic Engine Options
|
# Dynamic Engine Options
|
||||||
clear_layout(self.renderer_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.renderer_info.get(engine.name(), {}).get('system_info', {})
|
system_info = self.engine_info.get(engine.name(), {}).get('system_info', {})
|
||||||
self.current_engine_options = engine.ui_options(system_info=system_info)
|
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()
|
||||||
@@ -363,15 +363,15 @@ class NewRenderJobForm(QWidget):
|
|||||||
else:
|
else:
|
||||||
text_box = QLineEdit()
|
text_box = QLineEdit()
|
||||||
h_layout.addWidget(text_box)
|
h_layout.addWidget(text_box)
|
||||||
self.renderer_options_layout.addLayout(h_layout)
|
self.engine_options_layout.addLayout(h_layout)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def toggle_renderer_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)
|
self.project_group.setHidden(not enabled)
|
||||||
self.output_settings_group.setHidden(not enabled)
|
self.output_settings_group.setHidden(not enabled)
|
||||||
self.renderer_group.setHidden(not enabled)
|
self.engine_group.setHidden(not enabled)
|
||||||
self.notes_group.setHidden(not enabled)
|
self.notes_group.setHidden(not enabled)
|
||||||
if not enabled:
|
if not enabled:
|
||||||
self.cameras_group.setHidden(True)
|
self.cameras_group.setHidden(True)
|
||||||
@@ -386,7 +386,7 @@ class NewRenderJobForm(QWidget):
|
|||||||
self.submit_progress_label.setHidden(True)
|
self.submit_progress_label.setHidden(True)
|
||||||
self.process_progress_bar.setHidden(True)
|
self.process_progress_bar.setHidden(True)
|
||||||
self.process_label.setHidden(True)
|
self.process_label.setHidden(True)
|
||||||
self.toggle_renderer_enablement(True)
|
self.toggle_engine_enablement(True)
|
||||||
|
|
||||||
self.msg_box = QMessageBox()
|
self.msg_box = QMessageBox()
|
||||||
if not error_string:
|
if not error_string:
|
||||||
@@ -450,8 +450,8 @@ class SubmitWorker(QThread):
|
|||||||
try:
|
try:
|
||||||
hostname = self.window.server_input.currentText()
|
hostname = self.window.server_input.currentText()
|
||||||
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
||||||
'renderer': self.window.renderer_type.currentText().lower(),
|
'engine': self.window.engine_type.currentText().lower(),
|
||||||
'engine_version': self.window.renderer_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(),
|
'output_path': self.window.render_name_input.text(),
|
||||||
@@ -464,8 +464,8 @@ class SubmitWorker(QThread):
|
|||||||
'name': self.window.render_name_input.text()}
|
'name': self.window.render_name_input.text()}
|
||||||
|
|
||||||
# get the dynamic args
|
# get the dynamic args
|
||||||
for i in range(self.window.renderer_options_layout.count()):
|
for i in range(self.window.engine_options_layout.count()):
|
||||||
item = self.window.renderer_options_layout.itemAt(i)
|
item = self.window.engine_options_layout.itemAt(i)
|
||||||
layout = item.layout() # get the layout
|
layout = item.layout() # get the layout
|
||||||
for x in range(layout.count()):
|
for x in range(layout.count()):
|
||||||
z = layout.itemAt(x)
|
z = layout.itemAt(x)
|
||||||
@@ -497,7 +497,7 @@ class SubmitWorker(QThread):
|
|||||||
job_list = [job_json]
|
job_list = [job_json]
|
||||||
|
|
||||||
# presubmission tasks
|
# presubmission tasks
|
||||||
engine = EngineManager.engine_with_name(self.window.renderer_type.currentText().lower())
|
engine = EngineManager.engine_with_name(self.window.engine_type.currentText().lower())
|
||||||
input_path = engine().perform_presubmission_tasks(input_path)
|
input_path = engine().perform_presubmission_tasks(input_path)
|
||||||
# submit
|
# submit
|
||||||
err_msg = ""
|
err_msg = ""
|
||||||
@@ -93,7 +93,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()
|
raw_server_data = RenderServerProxy(self.hostname).get_engine_info()
|
||||||
if not raw_server_data:
|
if not raw_server_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ from src.render_queue import RenderQueue
|
|||||||
from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost
|
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 src.ui.add_job import NewRenderJobForm
|
from src.ui.add_job_window import NewRenderJobForm
|
||||||
from src.ui.console import ConsoleWindow
|
from src.ui.console_window import ConsoleWindow
|
||||||
from src.ui.engine_browser import EngineBrowserWindow
|
from src.ui.engine_browser import EngineBrowserWindow
|
||||||
from src.ui.log_viewer import LogViewer
|
from src.ui.log_window import LogViewer
|
||||||
from src.ui.widgets.menubar import MenuBar
|
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
|
||||||
@@ -306,12 +306,12 @@ 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', ''))
|
||||||
renderer = f"{job.get('renderer', '')}-{job.get('renderer_version')}"
|
engine_name = f"{job.get('renderer', '')}-{job.get('renderer_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'])
|
date_created_string = iso_datestring_to_formatted_datestring(job['date_created'])
|
||||||
|
|
||||||
items = [QTableWidgetItem(job['id']), QTableWidgetItem(name), QTableWidgetItem(renderer),
|
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(date_created_string)]
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ class MainWindow(QMainWindow):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def refresh_job_headers(self):
|
def refresh_job_headers(self):
|
||||||
self.job_list_view.setHorizontalHeaderLabels(["ID", "Name", "Renderer", "Priority", "Status",
|
self.job_list_view.setHorizontalHeaderLabels(["ID", "Name", "Engine", "Priority", "Status",
|
||||||
"Time Elapsed", "Frames", "Date Created"])
|
"Time Elapsed", "Frames", "Date Created"])
|
||||||
self.job_list_view.setColumnHidden(0, True)
|
self.job_list_view.setColumnHidden(0, True)
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ from src.engines.ffmpeg.ffmpeg_engine import FFMPEG
|
|||||||
|
|
||||||
def image_sequence_to_video(source_glob_pattern, output_path, framerate=24, encoder="prores_ks", profile=4,
|
def image_sequence_to_video(source_glob_pattern, output_path, framerate=24, encoder="prores_ks", profile=4,
|
||||||
start_frame=1):
|
start_frame=1):
|
||||||
subprocess.run([FFMPEG.default_renderer_path(), "-framerate", str(framerate), "-start_number",
|
subprocess.run([FFMPEG.default_engine_path(), "-framerate", str(framerate), "-start_number",
|
||||||
str(start_frame), "-i", f"{source_glob_pattern}", "-c:v", encoder, "-profile:v", str(profile),
|
str(start_frame), "-i", f"{source_glob_pattern}", "-c:v", encoder, "-profile:v", str(profile),
|
||||||
'-pix_fmt', 'yuva444p10le', output_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
'-pix_fmt', 'yuva444p10le', output_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
check=True)
|
check=True)
|
||||||
|
|
||||||
|
|
||||||
def save_first_frame(source_path, dest_path, max_width=1280):
|
def save_first_frame(source_path, dest_path, max_width=1280):
|
||||||
subprocess.run([FFMPEG.default_renderer_path(), '-i', source_path, '-vf', f'scale={max_width}:-1',
|
subprocess.run([FFMPEG.default_engine_path(), '-i', source_path, '-vf', f'scale={max_width}:-1',
|
||||||
'-vframes', '1', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
'-vframes', '1', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||||
|
|
||||||
|
|
||||||
def generate_thumbnail(source_path, dest_path, max_width=240, fps=12):
|
def generate_thumbnail(source_path, dest_path, max_width=240, fps=12):
|
||||||
subprocess.run([FFMPEG.default_renderer_path(), '-i', source_path, '-vf',
|
subprocess.run([FFMPEG.default_engine_path(), '-i', source_path, '-vf',
|
||||||
f"scale={max_width}:trunc(ow/a/2)*2,format=yuv420p", '-r', str(fps), '-c:v', 'libx264', '-preset',
|
f"scale={max_width}:trunc(ow/a/2)*2,format=yuv420p", '-r', str(fps), '-c:v', 'libx264', '-preset',
|
||||||
'ultrafast', '-an', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
'ultrafast', '-an', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user