mirror of
https://github.com/blw1138/Zordon.git
synced 2026-02-05 13:46:10 +00:00
Compare commits
2 Commits
6bfa5629d5
...
daf445ee9e
| Author | SHA1 | Date | |
|---|---|---|---|
| daf445ee9e | |||
| 574c6f0755 |
6
.github/workflows/create-executables.yml
vendored
6
.github/workflows/create-executables.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: sayyid5416/pyinstaller@v1
|
||||
with:
|
||||
python_ver: '3.11'
|
||||
spec: 'main.spec'
|
||||
spec: 'client.spec'
|
||||
requirements: 'requirements.txt'
|
||||
upload_exe_with_name: 'Zordon'
|
||||
pyinstaller-build-linux:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: sayyid5416/pyinstaller@v1
|
||||
with:
|
||||
python_ver: '3.11'
|
||||
spec: 'main.spec'
|
||||
spec: 'client.spec'
|
||||
requirements: 'requirements.txt'
|
||||
upload_exe_with_name: 'Zordon'
|
||||
pyinstaller-build-macos:
|
||||
@@ -33,6 +33,6 @@ jobs:
|
||||
uses: sayyid5416/pyinstaller@v1
|
||||
with:
|
||||
python_ver: '3.11'
|
||||
spec: 'main.spec'
|
||||
spec: 'client.spec'
|
||||
requirements: 'requirements.txt'
|
||||
upload_exe_with_name: 'Zordon'
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,4 +8,6 @@
|
||||
/.github/
|
||||
*.idea
|
||||
.DS_Store
|
||||
/venv/
|
||||
.env
|
||||
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()
|
||||
63
client.py
Executable file
63
client.py
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import logging
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
from server import start_server
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
def __setup_buffer_handler():
|
||||
# lazy load GUI frameworks
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
|
||||
class BufferingHandler(logging.Handler, QObject):
|
||||
new_record = pyqtSignal(str)
|
||||
|
||||
def __init__(self, capacity=100):
|
||||
logging.Handler.__init__(self)
|
||||
QObject.__init__(self)
|
||||
self.buffer = deque(maxlen=capacity) # Define a buffer with a fixed capacity
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
msg = self.format(record)
|
||||
self.buffer.append(msg) # Add message to the buffer
|
||||
self.new_record.emit(msg) # Emit signal
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def get_buffer(self):
|
||||
return list(self.buffer) # Return a copy of the buffer
|
||||
|
||||
buffer_handler = BufferingHandler()
|
||||
buffer_handler.setFormatter(logging.getLogger().handlers[0].formatter)
|
||||
new_logger = logging.getLogger()
|
||||
new_logger.addHandler(buffer_handler)
|
||||
return buffer_handler
|
||||
|
||||
|
||||
def __show_gui(buffer_handler):
|
||||
# lazy load GUI frameworks
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
# load application
|
||||
app: QApplication = QApplication(sys.argv)
|
||||
if app.style().objectName() != 'macos':
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# configure main window
|
||||
from src.ui.main_window import MainWindow
|
||||
window: MainWindow = MainWindow()
|
||||
window.buffer_handler = buffer_handler
|
||||
window.show()
|
||||
|
||||
return app.exec()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
local_server_thread = threading.Thread(target=start_server, args=[True], daemon=True)
|
||||
local_server_thread.start()
|
||||
__show_gui(__setup_buffer_handler())
|
||||
sys.exit()
|
||||
@@ -18,7 +18,7 @@ datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
['client.py'],
|
||||
pathex=[],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
120
job_launcher.py
Normal file
120
job_launcher.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, "renderer": 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('renderer')}-{running_job_data.get('renderer_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()
|
||||
7
main.py
7
main.py
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from src import init
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(init.run())
|
||||
141
server.py
141
server.py
@@ -1,5 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
from src.init import run
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import cpuinfo
|
||||
import psutil
|
||||
|
||||
from src.api.api_server import API_VERSION
|
||||
from src.api.api_server import start_api_server
|
||||
from src.api.preview_manager import PreviewManager
|
||||
from src.api.serverproxy_manager import ServerProxyManager
|
||||
from src.distributed_job_manager import DistributedJobManager
|
||||
from src.engines.engine_manager import EngineManager
|
||||
from src.render_queue import RenderQueue
|
||||
from src.utilities.config import Config
|
||||
from src.utilities.misc_helper import (get_gpu_info, system_safe_path, current_system_cpu, current_system_os,
|
||||
current_system_os_version, check_for_updates)
|
||||
from src.utilities.zeroconf_server import ZeroconfServer
|
||||
from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def start_server(skip_updates=False) -> int:
|
||||
"""Initializes the application and runs it.
|
||||
|
||||
Args:
|
||||
server_only: Run in server-only CLI mode. Default is False (runs in GUI mode).
|
||||
|
||||
Returns:
|
||||
int: The exit status code.
|
||||
"""
|
||||
|
||||
def existing_process(process_name):
|
||||
import psutil
|
||||
current_pid = os.getpid()
|
||||
current_process = psutil.Process(current_pid)
|
||||
for proc in psutil.process_iter(['pid', 'name', 'ppid']):
|
||||
proc_name = proc.info['name'].lower().rstrip('.exe')
|
||||
if proc_name == process_name.lower() and proc.info['pid'] != current_pid:
|
||||
if proc.info['pid'] == current_process.ppid():
|
||||
continue # parent process
|
||||
elif proc.info['ppid'] == current_pid:
|
||||
continue # child process
|
||||
else:
|
||||
return proc # unrelated process
|
||||
return None
|
||||
|
||||
# setup logging
|
||||
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
||||
level=Config.server_log_level.upper())
|
||||
logging.getLogger("requests").setLevel(logging.WARNING) # suppress noisy requests/urllib3 logging
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
# check for existing instance
|
||||
existing_proc = existing_process(APP_NAME)
|
||||
if existing_proc:
|
||||
logger.fatal(f"Another instance of {APP_NAME} is already running (pid: {existing_proc.pid})")
|
||||
sys.exit(1)
|
||||
|
||||
# check for updates
|
||||
if not skip_updates:
|
||||
update_thread = threading.Thread(target=check_for_updates, args=(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME,
|
||||
APP_VERSION))
|
||||
update_thread.start()
|
||||
|
||||
# main start
|
||||
logger.info(f"Starting {APP_NAME} Render Server")
|
||||
return_code = 0
|
||||
try:
|
||||
# Load Config YAML
|
||||
Config.setup_config_dir()
|
||||
Config.load_config(system_safe_path(os.path.join(Config.config_dir(), 'config.yaml')))
|
||||
|
||||
# configure default paths
|
||||
EngineManager.engines_path = system_safe_path(
|
||||
os.path.join(os.path.join(os.path.expanduser(Config.upload_folder),
|
||||
'engines')))
|
||||
os.makedirs(EngineManager.engines_path, exist_ok=True)
|
||||
PreviewManager.storage_path = system_safe_path(
|
||||
os.path.join(os.path.expanduser(Config.upload_folder), 'previews'))
|
||||
|
||||
# Debug info
|
||||
logger.debug(f"Upload directory: {os.path.expanduser(Config.upload_folder)}")
|
||||
logger.debug(f"Thumbs directory: {PreviewManager.storage_path}")
|
||||
logger.debug(f"Engines directory: {EngineManager.engines_path}")
|
||||
|
||||
# Set up the RenderQueue object
|
||||
RenderQueue.load_state(database_directory=system_safe_path(os.path.expanduser(Config.upload_folder)))
|
||||
ServerProxyManager.subscribe_to_listener()
|
||||
DistributedJobManager.subscribe_to_listener()
|
||||
|
||||
# check for updates for render engines if configured or on first launch
|
||||
if Config.update_engines_on_launch or not EngineManager.get_engines():
|
||||
EngineManager.update_all_engines()
|
||||
|
||||
# get hostname
|
||||
local_hostname = socket.gethostname()
|
||||
|
||||
# configure and start API server
|
||||
api_server = threading.Thread(target=start_api_server, args=(local_hostname,))
|
||||
api_server.daemon = True
|
||||
api_server.start()
|
||||
|
||||
# start zeroconf server
|
||||
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
||||
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
||||
'system_cpu_brand': cpuinfo.get_cpu_info()['brand_raw'],
|
||||
'system_cpu_cores': multiprocessing.cpu_count(),
|
||||
'system_os': current_system_os(),
|
||||
'system_os_version': current_system_os_version(),
|
||||
'system_memory': round(psutil.virtual_memory().total / (1024**3)), # in GB
|
||||
'gpu_info': get_gpu_info(),
|
||||
'api_version': API_VERSION}
|
||||
ZeroconfServer.start()
|
||||
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
||||
RenderQueue.start() # Start evaluating the render queue
|
||||
api_server.join()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.error(f"Unhandled exception: {e}")
|
||||
return_code = 1
|
||||
finally:
|
||||
# shut down gracefully
|
||||
logger.info(f"{APP_NAME} Render Server is preparing to shut down")
|
||||
try:
|
||||
RenderQueue.prepare_for_shutdown()
|
||||
except Exception as e:
|
||||
logger.exception(f"Exception during prepare for shutdown: {e}")
|
||||
ZeroconfServer.stop()
|
||||
logger.info(f"{APP_NAME} Render Server has shut down")
|
||||
return sys.exit(return_code)
|
||||
|
||||
if __name__ == '__main__':
|
||||
run(server_only=True)
|
||||
start_server()
|
||||
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
|
||||
)
|
||||
@@ -38,7 +38,7 @@ def handle_uploaded_project_files(request, jobs_list, upload_directory):
|
||||
uploaded_project = request.files.get('file', None)
|
||||
project_url = jobs_list[0].get('url', None)
|
||||
local_path = jobs_list[0].get('local_path', None)
|
||||
renderer = jobs_list[0].get('renderer')
|
||||
renderer = jobs_list[0]['renderer']
|
||||
downloaded_file_url = None
|
||||
|
||||
if uploaded_project and uploaded_project.filename:
|
||||
@@ -55,7 +55,7 @@ def handle_uploaded_project_files(request, jobs_list, upload_directory):
|
||||
# Prepare the local filepath
|
||||
cleaned_path_name = jobs_list[0].get('name', os.path.splitext(referred_name)[0]).replace(' ', '-')
|
||||
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)
|
||||
project_source_dir = os.path.join(job_dir, 'source')
|
||||
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}")
|
||||
|
||||
# supported_exts = RenderWorkerFactory.class_for_name(renderer).engine.supported_extensions
|
||||
# supported_exts = RenderWorkerFactory.class_for_name(engine).engine.supported_extensions
|
||||
# if supported_exts:
|
||||
# project_files = [file for file in project_files if any(file.endswith(ext) for ext in supported_exts)]
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ ssl._create_default_https_context = ssl._create_unverified_context # disable SS
|
||||
|
||||
API_VERSION = "1"
|
||||
|
||||
def start_server(hostname=None):
|
||||
def start_api_server(hostname=None):
|
||||
|
||||
# get hostname
|
||||
if not hostname:
|
||||
@@ -340,8 +340,8 @@ def delete_job(job_id):
|
||||
# Engine Info and Management:
|
||||
# --------------------------------------------
|
||||
|
||||
@server.get('/api/renderer_info')
|
||||
def renderer_info():
|
||||
@server.get('/api/engine_info')
|
||||
def engine_info():
|
||||
response_type = request.args.get('response_type', 'standard')
|
||||
if response_type not in ['full', 'standard']:
|
||||
raise ValueError(f"Invalid response_type: {response_type}")
|
||||
@@ -379,19 +379,19 @@ def renderer_info():
|
||||
return result
|
||||
|
||||
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
|
||||
|
||||
renderer_data = {}
|
||||
engine_data = {}
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {executor.submit(process_engine, engine): engine.name() for engine in EngineManager.supported_engines()}
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
renderer_data.update(result)
|
||||
engine_data.update(result)
|
||||
|
||||
return renderer_data
|
||||
return engine_data
|
||||
|
||||
|
||||
@server.get('/api/<engine_name>/is_available')
|
||||
@@ -442,27 +442,31 @@ def delete_engine_download():
|
||||
(f"Error deleting {json_data.get('engine')} {json_data.get('version')}", 500)
|
||||
|
||||
|
||||
@server.get('/api/renderer/<renderer>/args')
|
||||
def get_renderer_args(renderer):
|
||||
@server.get('/api/engine/<engine_name>/args')
|
||||
def get_engine_args(engine_name):
|
||||
try:
|
||||
renderer_engine_class = EngineManager.engine_with_name(renderer)
|
||||
return renderer_engine_class().get_arguments()
|
||||
engine_class = EngineManager.engine_with_name(engine_name)
|
||||
return engine_class().get_arguments()
|
||||
except LookupError:
|
||||
return f"Cannot find renderer '{renderer}'", 400
|
||||
return f"Cannot find engine '{engine_name}'", 400
|
||||
|
||||
|
||||
@server.get('/api/renderer/<renderer>/help')
|
||||
def get_renderer_help(renderer):
|
||||
@server.get('/api/engine/<engine_name>/help')
|
||||
def get_engine_help(engine_name):
|
||||
try:
|
||||
renderer_engine_class = EngineManager.engine_with_name(renderer)
|
||||
return renderer_engine_class().get_help()
|
||||
engine_class = EngineManager.engine_with_name(engine_name)
|
||||
return engine_class().get_help()
|
||||
except LookupError:
|
||||
return f"Cannot find renderer '{renderer}'", 400
|
||||
return f"Cannot find engine '{engine_name}'", 400
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Miscellaneous:
|
||||
# --------------------------------------------
|
||||
@server.get('/api/heartbeat')
|
||||
def heartbeat():
|
||||
return datetime.now().isoformat(), 200
|
||||
|
||||
@server.post('/api/job/<job_id>/send_subjob_update_notification')
|
||||
def subjob_update_notification(job_id):
|
||||
subjob_details = request.json
|
||||
|
||||
@@ -21,7 +21,6 @@ categories = [RenderStatus.RUNNING, RenderStatus.WAITING_FOR_SUBJOBS, RenderStat
|
||||
|
||||
logger = logging.getLogger()
|
||||
OFFLINE_MAX = 4
|
||||
LOOPBACK = '127.0.0.1'
|
||||
|
||||
|
||||
class RenderServerProxy:
|
||||
@@ -55,14 +54,18 @@ class RenderServerProxy:
|
||||
def __repr__(self):
|
||||
return f"<RenderServerProxy - {self.hostname}>"
|
||||
|
||||
def connect(self):
|
||||
return self.status()
|
||||
def check_connection(self):
|
||||
try:
|
||||
return self.request("heartbeat").ok
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def is_online(self):
|
||||
if self.__update_in_background:
|
||||
return self.__offline_flags < OFFLINE_MAX
|
||||
else:
|
||||
return self.get_status() is not None
|
||||
return self.check_connection()
|
||||
|
||||
def status(self):
|
||||
if not self.is_online():
|
||||
@@ -102,8 +105,7 @@ class RenderServerProxy:
|
||||
|
||||
def request(self, payload, timeout=5):
|
||||
from src.api.api_server import API_VERSION
|
||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
||||
return requests.get(f'http://{hostname}:{self.port}/api/{payload}', timeout=timeout,
|
||||
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout,
|
||||
headers={"X-API-Version": str(API_VERSION)})
|
||||
|
||||
# --------------------------------------------
|
||||
@@ -203,7 +205,7 @@ class RenderServerProxy:
|
||||
if self.is_localhost:
|
||||
jobs_with_path = [{'local_path': file_path, **item} for item in job_list]
|
||||
job_data = json.dumps(jobs_with_path)
|
||||
url = urljoin(f'http://{LOOPBACK}:{self.port}', '/api/add_job')
|
||||
url = urljoin(f'http://{self.hostname}:{self.port}', '/api/add_job')
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
return requests.post(url, data=job_data, headers=headers)
|
||||
|
||||
@@ -245,32 +247,32 @@ class RenderServerProxy:
|
||||
Returns:
|
||||
Response: The response from the server.
|
||||
"""
|
||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
||||
return requests.post(f'http://{hostname}:{self.port}/api/job/{parent_id}/send_subjob_update_notification',
|
||||
return requests.post(f'http://{self.hostname}:{self.port}/api/job/{parent_id}/send_subjob_update_notification',
|
||||
json=subjob.json())
|
||||
|
||||
# --------------------------------------------
|
||||
# Renderers:
|
||||
# Engines:
|
||||
# --------------------------------------------
|
||||
|
||||
def is_engine_available(self, engine_name):
|
||||
return self.request_data(f'{engine_name}/is_available')
|
||||
|
||||
def get_all_engines(self):
|
||||
# todo: this doesnt work
|
||||
return self.request_data('all_engines')
|
||||
|
||||
def get_renderer_info(self, response_type='standard', timeout=5):
|
||||
def get_engine_info(self, response_type='standard', timeout=5):
|
||||
"""
|
||||
Fetches renderer information from the server.
|
||||
Fetches engine information from the server.
|
||||
|
||||
Args:
|
||||
response_type (str, optional): Returns standard or full version of renderer info
|
||||
response_type (str, optional): Returns standard or full version of engine info
|
||||
timeout (int, optional): The number of seconds to wait for a response from the server. Defaults to 5.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the renderer information.
|
||||
dict: A dictionary containing the engine information.
|
||||
"""
|
||||
all_data = self.request_data(f"renderer_info?response_type={response_type}", timeout=timeout)
|
||||
all_data = self.request_data(f"engine_info?response_type={response_type}", timeout=timeout)
|
||||
return all_data
|
||||
|
||||
def delete_engine(self, engine, version, system_cpu=None):
|
||||
@@ -286,21 +288,18 @@ class RenderServerProxy:
|
||||
Response: The response from the server.
|
||||
"""
|
||||
form_data = {'engine': engine, 'version': version, 'system_cpu': system_cpu}
|
||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
||||
return requests.post(f'http://{hostname}:{self.port}/api/delete_engine', json=form_data)
|
||||
return requests.post(f'http://{self.hostname}:{self.port}/api/delete_engine', json=form_data)
|
||||
|
||||
# --------------------------------------------
|
||||
# Download Files:
|
||||
# --------------------------------------------
|
||||
|
||||
def download_all_job_files(self, job_id, save_path):
|
||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
||||
url = f"http://{hostname}:{self.port}/api/job/{job_id}/download_all"
|
||||
url = f"http://{self.hostname}:{self.port}/api/job/{job_id}/download_all"
|
||||
return self.__download_file_from_url(url, output_filepath=save_path)
|
||||
|
||||
def download_job_file(self, job_id, job_filename, save_path):
|
||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
||||
url = f"http://{hostname}:{self.port}/api/job/{job_id}/download?filename={job_filename}"
|
||||
url = f"http://{self.hostname}:{self.port}/api/job/{job_id}/download?filename={job_filename}"
|
||||
return self.__download_file_from_url(url, output_filepath=save_path)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -156,7 +156,7 @@ class DistributedJobManager:
|
||||
logger.debug(f"New job output path: {output_path}")
|
||||
|
||||
# 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,
|
||||
output_path=output_path,
|
||||
engine_version=new_job_attributes.get('engine_version'),
|
||||
@@ -303,14 +303,14 @@ class DistributedJobManager:
|
||||
|
||||
Args:
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
|
||||
# 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)
|
||||
# skip if theres no external servers found
|
||||
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['start_frame'] = server_data['frame_range'][0]
|
||||
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']}-"
|
||||
f"{subjob['end_frame']} to {server_hostname}")
|
||||
post_results = RenderServerProxy(server_hostname).post_job_to_server(
|
||||
|
||||
@@ -8,7 +8,7 @@ class AERender(BaseRenderEngine):
|
||||
def version(self):
|
||||
version = None
|
||||
try:
|
||||
render_path = self.renderer_path()
|
||||
render_path = self.engine_path()
|
||||
if render_path:
|
||||
ver_out = subprocess.check_output([render_path, '-version'], timeout=SUBPROCESS_TIMEOUT)
|
||||
version = ver_out.decode('utf-8').split(" ")[-1].strip()
|
||||
|
||||
@@ -35,7 +35,7 @@ class Blender(BaseRenderEngine):
|
||||
def version(self):
|
||||
version = None
|
||||
try:
|
||||
render_path = self.renderer_path()
|
||||
render_path = self.engine_path()
|
||||
if render_path:
|
||||
ver_out = subprocess.check_output([render_path, '-v'], timeout=SUBPROCESS_TIMEOUT,
|
||||
creationflags=_creationflags)
|
||||
@@ -52,7 +52,7 @@ class Blender(BaseRenderEngine):
|
||||
def run_python_expression(self, project_path, python_expression, timeout=None):
|
||||
if os.path.exists(project_path):
|
||||
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)
|
||||
except Exception as 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}')
|
||||
|
||||
try:
|
||||
command = [self.renderer_path(), '-b', '--python', script_path]
|
||||
command = [self.engine_path(), '-b', '--python', script_path]
|
||||
if project_path:
|
||||
command.insert(2, project_path)
|
||||
result = subprocess.run(command, capture_output=True, timeout=timeout, creationflags=_creationflags)
|
||||
@@ -132,7 +132,7 @@ class Blender(BaseRenderEngine):
|
||||
return None
|
||||
|
||||
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()
|
||||
|
||||
options = {}
|
||||
@@ -179,7 +179,7 @@ class Blender(BaseRenderEngine):
|
||||
logger.error("GPU data not found in the output.")
|
||||
|
||||
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()
|
||||
render_engines = [x.strip() for x in engine_output.split('Blender Engine Listing:')[-1].strip().splitlines()]
|
||||
return render_engines
|
||||
|
||||
@@ -26,7 +26,7 @@ class BlenderRenderWorker(BaseRenderWorker):
|
||||
|
||||
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
|
||||
cmd.append('-b')
|
||||
cmd.append(self.input_path)
|
||||
|
||||
@@ -9,7 +9,7 @@ SUBPROCESS_TIMEOUT = 5
|
||||
|
||||
class BaseRenderEngine(object):
|
||||
"""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:
|
||||
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):
|
||||
self.custom_renderer_path = custom_path
|
||||
if not self.renderer_path() or not os.path.exists(self.renderer_path()):
|
||||
raise FileNotFoundError(f"Cannot find path to renderer for {self.name()} instance: {self.renderer_path()}")
|
||||
self.custom_engine_path = custom_path
|
||||
if not self.engine_path() or not os.path.exists(self.engine_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):
|
||||
logger.warning(f"Path is not executable. Setting permissions to 755 for {self.renderer_path()}")
|
||||
os.chmod(self.renderer_path(), 0o755)
|
||||
if not os.access(self.engine_path(), os.X_OK):
|
||||
logger.warning(f"Path is not executable. Setting permissions to 755 for {self.engine_path()}")
|
||||
os.chmod(self.engine_path(), 0o755)
|
||||
|
||||
def version(self):
|
||||
"""Return the version number as a string.
|
||||
@@ -60,7 +60,7 @@ class BaseRenderEngine(object):
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
list[str]: A list of strings representing the available output formats.
|
||||
@@ -83,20 +83,20 @@ class BaseRenderEngine(object):
|
||||
return []
|
||||
|
||||
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.
|
||||
Override this method if the renderer uses a different help flag.
|
||||
This method runs the engine's help command (default: '-h') and captures the output.
|
||||
Override this method if the engine uses a different help flag.
|
||||
|
||||
Returns:
|
||||
str: The help documentation as a string.
|
||||
|
||||
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:
|
||||
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
|
||||
help_doc = subprocess.check_output([path, '-h'], stderr=subprocess.STDOUT,
|
||||
timeout=SUBPROCESS_TIMEOUT, creationflags=creationflags).decode('utf-8')
|
||||
@@ -141,15 +141,15 @@ class BaseRenderEngine(object):
|
||||
# Do Not Override These Methods:
|
||||
# --------------------------------------------
|
||||
|
||||
def renderer_path(self):
|
||||
return self.custom_renderer_path or self.default_renderer_path()
|
||||
def engine_path(self):
|
||||
return self.custom_engine_path or self.default_engine_path()
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return str(cls.__name__).lower()
|
||||
|
||||
@classmethod
|
||||
def default_renderer_path(cls):
|
||||
def default_engine_path(cls):
|
||||
path = None
|
||||
try: # Linux and macOS
|
||||
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)
|
||||
start_time = Column(DateTime, nullable=True)
|
||||
end_time = Column(DateTime, nullable=True)
|
||||
renderer = Column(String)
|
||||
renderer_version = Column(String)
|
||||
renderer_path = Column(String)
|
||||
engine_name = Column(String)
|
||||
engine_version = Column(String)
|
||||
engine_path = Column(String)
|
||||
priority = Column(Integer)
|
||||
project_length = Column(Integer)
|
||||
start_frame = Column(Integer)
|
||||
@@ -46,8 +46,6 @@ class BaseRenderWorker(Base):
|
||||
file_hash = Column(String)
|
||||
_status = Column(String)
|
||||
|
||||
engine = None
|
||||
|
||||
# --------------------------------------------
|
||||
# Required Overrides for Subclasses:
|
||||
# --------------------------------------------
|
||||
@@ -57,7 +55,7 @@ class BaseRenderWorker(Base):
|
||||
|
||||
if not ignore_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)
|
||||
raise ValueError(err_meg)
|
||||
if not self.engine:
|
||||
@@ -74,10 +72,10 @@ class BaseRenderWorker(Base):
|
||||
self.output_path = output_path
|
||||
self.args = args or {}
|
||||
self.date_created = datetime.now()
|
||||
self.renderer = self.engine.name()
|
||||
self.renderer_path = engine_path
|
||||
self.renderer_version = self.engine(engine_path).version()
|
||||
self.custom_renderer_path = None
|
||||
self.engine_name = self.engine.name()
|
||||
self.engine_path = engine_path
|
||||
self.engine_version = self.engine(engine_path).version()
|
||||
self.custom_engine_path = None
|
||||
self.priority = priority
|
||||
self.parent = parent
|
||||
self.children = {}
|
||||
@@ -116,17 +114,17 @@ class BaseRenderWorker(Base):
|
||||
raise NotImplementedError("generate_worker_subprocess not implemented")
|
||||
|
||||
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
|
||||
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:
|
||||
1. Update value of self.current_frame
|
||||
2. Call self._send_frame_complete_notification()
|
||||
|
||||
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:
|
||||
NotImplementedError: If the method is not overridden in a subclass.
|
||||
@@ -152,7 +150,7 @@ class BaseRenderWorker(Base):
|
||||
# --------------------------------------------
|
||||
|
||||
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
|
||||
def total_frames(self):
|
||||
@@ -215,7 +213,7 @@ class BaseRenderWorker(Base):
|
||||
self.errors.append(msg)
|
||||
return
|
||||
|
||||
if not os.path.exists(self.renderer_path):
|
||||
if not os.path.exists(self.engine_path):
|
||||
self.status = RenderStatus.ERROR
|
||||
msg = f'Cannot find render engine path for {self.engine.name()}'
|
||||
logger.error(msg)
|
||||
@@ -269,14 +267,14 @@ class BaseRenderWorker(Base):
|
||||
logger.error(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:
|
||||
err_msg = (f"{self.engine.name()} render exited ok, but file count has not increased. "
|
||||
f"Count is still {len(self.file_list())}")
|
||||
log_file.write(f'Error: {err_msg}\n\n')
|
||||
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
|
||||
|
||||
def __run__wait_for_subjobs(self, logfile):
|
||||
@@ -302,7 +300,7 @@ class BaseRenderWorker(Base):
|
||||
with open(self.log_path(), "a") as log_file:
|
||||
|
||||
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)
|
||||
log_file.write(f"\n")
|
||||
if not self.children:
|
||||
@@ -493,8 +491,8 @@ class BaseRenderWorker(Base):
|
||||
'file_hash': self.file_hash,
|
||||
'percent_complete': self.percent_complete(),
|
||||
'file_count': len(self.file_list()),
|
||||
'renderer': self.renderer,
|
||||
'renderer_version': self.renderer_version,
|
||||
'engine': self.engine_name,
|
||||
'engine_version': self.engine_version,
|
||||
'errors': getattr(self, 'errors', None),
|
||||
'start_frame': self.start_frame,
|
||||
'end_frame': self.end_frame,
|
||||
|
||||
@@ -12,7 +12,7 @@ logger = logging.getLogger()
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -88,7 +88,7 @@ class EngineManager:
|
||||
'version': version or 'error',
|
||||
'system_os': current_system_os(),
|
||||
'cpu': current_system_cpu(),
|
||||
'path': eng.default_renderer_path(),
|
||||
'path': eng.default_engine_path(),
|
||||
'type': 'system'
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class EngineManager:
|
||||
futures = {
|
||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||
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):
|
||||
@@ -240,14 +240,14 @@ class EngineManager:
|
||||
thread.start()
|
||||
|
||||
@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
|
||||
all_versions = cls.all_versions_for_engine(renderer)
|
||||
all_versions = cls.all_versions_for_engine(engine_name)
|
||||
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
|
||||
engine_path = None
|
||||
@@ -259,9 +259,9 @@ class EngineManager:
|
||||
|
||||
# Download the required engine if not found locally
|
||||
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:
|
||||
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']
|
||||
logger.info("Engine downloaded. Creating worker.")
|
||||
else:
|
||||
|
||||
@@ -24,7 +24,7 @@ class FFMPEG(BaseRenderEngine):
|
||||
return FFMPEGUI.get_options(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'))
|
||||
found = re.findall(r'extensions that .* is allowed to access \(default "(.*)"', help_text)
|
||||
found_extensions = set()
|
||||
@@ -35,7 +35,7 @@ class FFMPEG(BaseRenderEngine):
|
||||
def version(self):
|
||||
version = None
|
||||
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')
|
||||
match = re.match(r".*version\s*([\w.*]+)\W*", ver_out)
|
||||
if match:
|
||||
@@ -82,7 +82,7 @@ class FFMPEG(BaseRenderEngine):
|
||||
return None
|
||||
|
||||
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')
|
||||
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)]
|
||||
@@ -94,7 +94,7 @@ class FFMPEG(BaseRenderEngine):
|
||||
|
||||
def get_all_formats(self):
|
||||
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,
|
||||
creationflags=_creationflags).decode('utf-8')
|
||||
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
|
||||
muxer_flag = 'muxer' if 'E' in ffmpeg_format['type'] else 'demuxer'
|
||||
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')
|
||||
pattern = r"Common extensions: (\w+)"
|
||||
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()]
|
||||
|
||||
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,
|
||||
timeout=SUBPROCESS_TIMEOUT, creationflags=_creationflags).decode('utf-8')
|
||||
match = re.findall(r'frame=\s*(\d+)', raw_stdout)
|
||||
@@ -131,7 +131,7 @@ class FFMPEG(BaseRenderEngine):
|
||||
return -1
|
||||
|
||||
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'))
|
||||
lines = help_text.splitlines()
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class FFMPEGRenderWorker(BaseRenderWorker):
|
||||
|
||||
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
|
||||
if self.args.get('x_resolution', None) and self.args.get('y_resolution', None):
|
||||
|
||||
195
src/init.py
195
src/init.py
@@ -1,195 +0,0 @@
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
import cpuinfo
|
||||
import psutil
|
||||
|
||||
from src.api.api_server import API_VERSION
|
||||
from src.api.api_server import start_server
|
||||
from src.api.preview_manager import PreviewManager
|
||||
from src.api.serverproxy_manager import ServerProxyManager
|
||||
from src.distributed_job_manager import DistributedJobManager
|
||||
from src.engines.engine_manager import EngineManager
|
||||
from src.render_queue import RenderQueue
|
||||
from src.utilities.config import Config
|
||||
from src.utilities.misc_helper import (get_gpu_info, system_safe_path, current_system_cpu, current_system_os,
|
||||
current_system_os_version, check_for_updates)
|
||||
from src.utilities.zeroconf_server import ZeroconfServer
|
||||
from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def run(server_only=False) -> int:
|
||||
"""Initializes the application and runs it.
|
||||
|
||||
Args:
|
||||
server_only: Run in server-only CLI mode. Default is False (runs in GUI mode).
|
||||
|
||||
Returns:
|
||||
int: The exit status code.
|
||||
"""
|
||||
|
||||
def existing_process(process_name):
|
||||
import psutil
|
||||
current_pid = os.getpid()
|
||||
current_process = psutil.Process(current_pid)
|
||||
for proc in psutil.process_iter(['pid', 'name', 'ppid']):
|
||||
proc_name = proc.info['name'].lower().rstrip('.exe')
|
||||
if proc_name == process_name.lower() and proc.info['pid'] != current_pid:
|
||||
if proc.info['pid'] == current_process.ppid():
|
||||
continue # parent process
|
||||
elif proc.info['ppid'] == current_pid:
|
||||
continue # child process
|
||||
else:
|
||||
return proc # unrelated process
|
||||
return None
|
||||
|
||||
# setup logging
|
||||
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
||||
level=Config.server_log_level.upper())
|
||||
logging.getLogger("requests").setLevel(logging.WARNING) # suppress noisy requests/urllib3 logging
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
# check for existing instance
|
||||
existing_proc = existing_process(APP_NAME)
|
||||
if existing_proc:
|
||||
logger.fatal(f"Another instance of {APP_NAME} is already running (pid: {existing_proc.pid})")
|
||||
sys.exit(1)
|
||||
|
||||
# Setup logging for console ui
|
||||
buffer_handler = __setup_buffer_handler() if not server_only else None
|
||||
|
||||
# check for updates
|
||||
update_thread = threading.Thread(target=check_for_updates, args=(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME,
|
||||
APP_VERSION))
|
||||
update_thread.start()
|
||||
|
||||
# main start
|
||||
logger.info(f"Starting {APP_NAME} Render Server")
|
||||
return_code = 0
|
||||
try:
|
||||
# Load Config YAML
|
||||
Config.setup_config_dir()
|
||||
Config.load_config(system_safe_path(os.path.join(Config.config_dir(), 'config.yaml')))
|
||||
|
||||
# configure default paths
|
||||
EngineManager.engines_path = system_safe_path(
|
||||
os.path.join(os.path.join(os.path.expanduser(Config.upload_folder),
|
||||
'engines')))
|
||||
os.makedirs(EngineManager.engines_path, exist_ok=True)
|
||||
PreviewManager.storage_path = system_safe_path(
|
||||
os.path.join(os.path.expanduser(Config.upload_folder), 'previews'))
|
||||
|
||||
# Debug info
|
||||
logger.debug(f"Upload directory: {os.path.expanduser(Config.upload_folder)}")
|
||||
logger.debug(f"Thumbs directory: {PreviewManager.storage_path}")
|
||||
logger.debug(f"Engines directory: {EngineManager.engines_path}")
|
||||
|
||||
# Set up the RenderQueue object
|
||||
RenderQueue.load_state(database_directory=system_safe_path(os.path.expanduser(Config.upload_folder)))
|
||||
ServerProxyManager.subscribe_to_listener()
|
||||
DistributedJobManager.subscribe_to_listener()
|
||||
|
||||
# check for updates for render engines if configured or on first launch
|
||||
if Config.update_engines_on_launch or not EngineManager.get_engines():
|
||||
EngineManager.update_all_engines()
|
||||
|
||||
# get hostname
|
||||
local_hostname = socket.gethostname()
|
||||
local_hostname = local_hostname + (".local" if not local_hostname.endswith(".local") else "")
|
||||
|
||||
# configure and start API server
|
||||
api_server = threading.Thread(target=start_server, args=(local_hostname,))
|
||||
api_server.daemon = True
|
||||
api_server.start()
|
||||
|
||||
# start zeroconf server
|
||||
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
||||
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
||||
'system_cpu_brand': cpuinfo.get_cpu_info()['brand_raw'],
|
||||
'system_cpu_cores': multiprocessing.cpu_count(),
|
||||
'system_os': current_system_os(),
|
||||
'system_os_version': current_system_os_version(),
|
||||
'system_memory': round(psutil.virtual_memory().total / (1024**3)), # in GB
|
||||
'gpu_info': get_gpu_info(),
|
||||
'api_version': API_VERSION}
|
||||
ZeroconfServer.start()
|
||||
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
||||
RenderQueue.start() # Start evaluating the render queue
|
||||
|
||||
# start in gui or server only (cli) mode
|
||||
logger.debug(f"Launching in {'server only' if server_only else 'GUI'} mode")
|
||||
if server_only: # CLI only
|
||||
api_server.join()
|
||||
else: # GUI
|
||||
return_code = __show_gui(buffer_handler)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.error(f"Unhandled exception: {e}")
|
||||
return_code = 1
|
||||
finally:
|
||||
# shut down gracefully
|
||||
logger.info(f"{APP_NAME} Render Server is preparing to shut down")
|
||||
try:
|
||||
RenderQueue.prepare_for_shutdown()
|
||||
except Exception as e:
|
||||
logger.exception(f"Exception during prepare for shutdown: {e}")
|
||||
ZeroconfServer.stop()
|
||||
logger.info(f"{APP_NAME} Render Server has shut down")
|
||||
return sys.exit(return_code)
|
||||
|
||||
|
||||
def __setup_buffer_handler():
|
||||
# lazy load GUI frameworks
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
|
||||
class BufferingHandler(logging.Handler, QObject):
|
||||
new_record = pyqtSignal(str)
|
||||
|
||||
def __init__(self, capacity=100):
|
||||
logging.Handler.__init__(self)
|
||||
QObject.__init__(self)
|
||||
self.buffer = deque(maxlen=capacity) # Define a buffer with a fixed capacity
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
msg = self.format(record)
|
||||
self.buffer.append(msg) # Add message to the buffer
|
||||
self.new_record.emit(msg) # Emit signal
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def get_buffer(self):
|
||||
return list(self.buffer) # Return a copy of the buffer
|
||||
|
||||
buffer_handler = BufferingHandler()
|
||||
buffer_handler.setFormatter(logging.getLogger().handlers[0].formatter)
|
||||
new_logger = logging.getLogger()
|
||||
new_logger.addHandler(buffer_handler)
|
||||
return buffer_handler
|
||||
|
||||
|
||||
def __show_gui(buffer_handler):
|
||||
# lazy load GUI frameworks
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
# load application
|
||||
app: QApplication = QApplication(sys.argv)
|
||||
if app.style().objectName() != 'macos':
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# configure main window
|
||||
from src.ui.main_window import MainWindow
|
||||
window: MainWindow = MainWindow()
|
||||
window.buffer_handler = buffer_handler
|
||||
window.show()
|
||||
|
||||
return app.exec()
|
||||
@@ -46,7 +46,7 @@ class RenderQueue:
|
||||
try:
|
||||
not_started = cls.jobs_with_status(RenderStatus.NOT_STARTED, priority_sorted=True)
|
||||
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)
|
||||
|
||||
scheduled = cls.jobs_with_status(RenderStatus.SCHEDULED, priority_sorted=True)
|
||||
@@ -145,7 +145,7 @@ class RenderQueue:
|
||||
@classmethod
|
||||
def renderer_instances(cls):
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -14,7 +14,7 @@ from requests import Response
|
||||
|
||||
from src.api.server_proxy import RenderServerProxy
|
||||
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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class NewRenderJobForm(QWidget):
|
||||
self.notes_group = None
|
||||
self.frame_rate_input = None
|
||||
self.resolution_x_input = None
|
||||
self.renderer_group = None
|
||||
self.engine_group = None
|
||||
self.output_settings_group = None
|
||||
self.resolution_y_input = None
|
||||
self.project_path = project_path
|
||||
@@ -34,17 +34,17 @@ class NewRenderJobForm(QWidget):
|
||||
self.load_file_group = None
|
||||
self.current_engine_options = None
|
||||
self.file_format_combo = None
|
||||
self.renderer_options_layout = None
|
||||
self.engine_options_layout = None
|
||||
self.cameras_list = None
|
||||
self.cameras_group = None
|
||||
self.renderer_version_combo = None
|
||||
self.engine_version_combo = None
|
||||
self.worker_thread = None
|
||||
self.msg_box = None
|
||||
self.engine_help_viewer = None
|
||||
self.raw_args = None
|
||||
self.submit_progress_label = None
|
||||
self.submit_progress = None
|
||||
self.renderer_type = None
|
||||
self.engine_type = None
|
||||
self.process_label = None
|
||||
self.process_progress_bar = None
|
||||
self.splitjobs_same_os = None
|
||||
@@ -62,13 +62,13 @@ class NewRenderJobForm(QWidget):
|
||||
|
||||
# Job / Server Data
|
||||
self.server_proxy = RenderServerProxy(socket.gethostname())
|
||||
self.renderer_info = None
|
||||
self.engine_info = None
|
||||
self.project_info = None
|
||||
|
||||
# Setup
|
||||
self.setWindowTitle("New Job")
|
||||
self.setup_ui()
|
||||
self.update_renderer_info()
|
||||
self.update_engine_info()
|
||||
self.setup_project()
|
||||
|
||||
# get renderer info in bg thread
|
||||
@@ -182,33 +182,33 @@ class NewRenderJobForm(QWidget):
|
||||
# add group to layout
|
||||
main_layout.addWidget(self.output_settings_group)
|
||||
|
||||
# Renderer Group
|
||||
self.renderer_group = QGroupBox("Renderer Settings")
|
||||
renderer_group_layout = QVBoxLayout(self.renderer_group)
|
||||
renderer_layout = QHBoxLayout()
|
||||
renderer_layout.addWidget(QLabel("Renderer:"))
|
||||
self.renderer_type = QComboBox()
|
||||
self.renderer_type.currentIndexChanged.connect(self.renderer_changed)
|
||||
renderer_layout.addWidget(self.renderer_type)
|
||||
# Engine Group
|
||||
self.engine_group = QGroupBox("Engine Settings")
|
||||
engine_group_layout = QVBoxLayout(self.engine_group)
|
||||
engine_layout = QHBoxLayout()
|
||||
engine_layout.addWidget(QLabel("Engine:"))
|
||||
self.engine_type = QComboBox()
|
||||
self.engine_type.currentIndexChanged.connect(self.engine_changed)
|
||||
engine_layout.addWidget(self.engine_type)
|
||||
# Version
|
||||
renderer_layout.addWidget(QLabel("Version:"))
|
||||
self.renderer_version_combo = QComboBox()
|
||||
self.renderer_version_combo.addItem('latest')
|
||||
renderer_layout.addWidget(self.renderer_version_combo)
|
||||
renderer_group_layout.addLayout(renderer_layout)
|
||||
engine_layout.addWidget(QLabel("Version:"))
|
||||
self.engine_version_combo = QComboBox()
|
||||
self.engine_version_combo.addItem('latest')
|
||||
engine_layout.addWidget(self.engine_version_combo)
|
||||
engine_group_layout.addLayout(engine_layout)
|
||||
# dynamic options
|
||||
self.renderer_options_layout = QVBoxLayout()
|
||||
renderer_group_layout.addLayout(self.renderer_options_layout)
|
||||
self.engine_options_layout = QVBoxLayout()
|
||||
engine_group_layout.addLayout(self.engine_options_layout)
|
||||
# Raw Args
|
||||
raw_args_layout = QHBoxLayout(self.renderer_group)
|
||||
raw_args_layout = QHBoxLayout(self.engine_group)
|
||||
raw_args_layout.addWidget(QLabel("Raw Args:"))
|
||||
self.raw_args = QLineEdit()
|
||||
raw_args_layout.addWidget(self.raw_args)
|
||||
args_help_button = QPushButton("?")
|
||||
args_help_button.clicked.connect(self.args_help_button_clicked)
|
||||
raw_args_layout.addWidget(args_help_button)
|
||||
renderer_group_layout.addLayout(raw_args_layout)
|
||||
main_layout.addWidget(self.renderer_group)
|
||||
engine_group_layout.addLayout(raw_args_layout)
|
||||
main_layout.addWidget(self.engine_group)
|
||||
|
||||
# Cameras Group
|
||||
self.cameras_group = QGroupBox("Cameras")
|
||||
@@ -240,28 +240,28 @@ class NewRenderJobForm(QWidget):
|
||||
self.submit_progress_label.setHidden(True)
|
||||
main_layout.addWidget(self.submit_progress_label)
|
||||
|
||||
self.toggle_renderer_enablement(False)
|
||||
self.toggle_engine_enablement(False)
|
||||
|
||||
def update_renderer_info(self):
|
||||
# get the renderer info and add them all to the ui
|
||||
self.renderer_info = self.server_proxy.get_renderer_info(response_type='full')
|
||||
self.renderer_type.addItems(self.renderer_info.keys())
|
||||
# select the best renderer for the file type
|
||||
def update_engine_info(self):
|
||||
# get the engine info and add them all to the ui
|
||||
self.engine_info = self.server_proxy.get_engine_info(response_type='full')
|
||||
self.engine_type.addItems(self.engine_info.keys())
|
||||
# select the best engine for the file type
|
||||
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
|
||||
self.renderer_changed()
|
||||
self.engine_changed()
|
||||
|
||||
def renderer_changed(self):
|
||||
def engine_changed(self):
|
||||
# load the version numbers
|
||||
current_renderer = self.renderer_type.currentText().lower() or self.renderer_type.itemText(0)
|
||||
self.renderer_version_combo.clear()
|
||||
self.renderer_version_combo.addItem('latest')
|
||||
current_engine = self.engine_type.currentText().lower() or self.engine_type.itemText(0)
|
||||
self.engine_version_combo.clear()
|
||||
self.engine_version_combo.addItem('latest')
|
||||
self.file_format_combo.clear()
|
||||
if current_renderer:
|
||||
renderer_vers = [version_info['version'] for version_info in self.renderer_info[current_renderer]['versions']]
|
||||
self.renderer_version_combo.addItems(renderer_vers)
|
||||
self.file_format_combo.addItems(self.renderer_info[current_renderer]['supported_export_formats'])
|
||||
if current_engine:
|
||||
engine_vers = [version_info['version'] for version_info in self.engine_info[current_engine]['versions']]
|
||||
self.engine_version_combo.addItems(engine_vers)
|
||||
self.file_format_combo.addItems(self.engine_info[current_engine]['supported_export_formats'])
|
||||
|
||||
def update_server_list(self):
|
||||
clients = ZeroconfServer.found_hostnames()
|
||||
@@ -278,7 +278,7 @@ class NewRenderJobForm(QWidget):
|
||||
# UI stuff on main thread
|
||||
self.process_progress_bar.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 = output_name.replace(' ', '_')
|
||||
@@ -296,8 +296,8 @@ class NewRenderJobForm(QWidget):
|
||||
self.render_name_input.setText(directory)
|
||||
|
||||
def args_help_button_clicked(self):
|
||||
url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/renderer/'
|
||||
f'{self.renderer_type.currentText()}/help')
|
||||
url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/engine/'
|
||||
f'{self.engine_type.currentText()}/help')
|
||||
self.engine_help_viewer = EngineHelpViewer(url)
|
||||
self.engine_help_viewer.show()
|
||||
|
||||
@@ -306,20 +306,20 @@ class NewRenderJobForm(QWidget):
|
||||
def post_get_project_info_update(self):
|
||||
"""Called by the GetProjectInfoWorker - Do not call directly."""
|
||||
try:
|
||||
# Set the best renderer we can find
|
||||
# Set the best engine we can find
|
||||
input_path = self.scene_file_input.text()
|
||||
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:
|
||||
self.renderer_type.setCurrentIndex(engine_index)
|
||||
self.engine_type.setCurrentIndex(engine_index)
|
||||
else:
|
||||
self.renderer_type.setCurrentIndex(0) #todo: find out why we don't have renderer info yet
|
||||
# not ideal but if we don't have the renderer info we have to pick something
|
||||
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 engine info we have to pick something
|
||||
|
||||
# cleanup progress UI
|
||||
self.load_file_group.setHidden(True)
|
||||
self.toggle_renderer_enablement(True)
|
||||
self.toggle_engine_enablement(True)
|
||||
|
||||
# Load scene data
|
||||
self.start_frame_input.setValue(self.project_info.get('frame_start'))
|
||||
@@ -347,9 +347,9 @@ class NewRenderJobForm(QWidget):
|
||||
self.cameras_group.setHidden(True)
|
||||
|
||||
# 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
|
||||
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)
|
||||
for option in self.current_engine_options:
|
||||
h_layout = QHBoxLayout()
|
||||
@@ -363,15 +363,15 @@ class NewRenderJobForm(QWidget):
|
||||
else:
|
||||
text_box = QLineEdit()
|
||||
h_layout.addWidget(text_box)
|
||||
self.renderer_options_layout.addLayout(h_layout)
|
||||
self.engine_options_layout.addLayout(h_layout)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def toggle_renderer_enablement(self, enabled=False):
|
||||
def toggle_engine_enablement(self, enabled=False):
|
||||
"""Toggle on/off all the render settings"""
|
||||
self.project_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)
|
||||
if not enabled:
|
||||
self.cameras_group.setHidden(True)
|
||||
@@ -386,7 +386,7 @@ class NewRenderJobForm(QWidget):
|
||||
self.submit_progress_label.setHidden(True)
|
||||
self.process_progress_bar.setHidden(True)
|
||||
self.process_label.setHidden(True)
|
||||
self.toggle_renderer_enablement(True)
|
||||
self.toggle_engine_enablement(True)
|
||||
|
||||
self.msg_box = QMessageBox()
|
||||
if not error_string:
|
||||
@@ -450,8 +450,8 @@ class SubmitWorker(QThread):
|
||||
try:
|
||||
hostname = self.window.server_input.currentText()
|
||||
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
||||
'renderer': self.window.renderer_type.currentText().lower(),
|
||||
'engine_version': self.window.renderer_version_combo.currentText(),
|
||||
'engine': self.window.engine_type.currentText().lower(),
|
||||
'engine_version': self.window.engine_version_combo.currentText(),
|
||||
'args': {'raw': self.window.raw_args.text(),
|
||||
'export_format': self.window.file_format_combo.currentText()},
|
||||
'output_path': self.window.render_name_input.text(),
|
||||
@@ -464,8 +464,8 @@ class SubmitWorker(QThread):
|
||||
'name': self.window.render_name_input.text()}
|
||||
|
||||
# get the dynamic args
|
||||
for i in range(self.window.renderer_options_layout.count()):
|
||||
item = self.window.renderer_options_layout.itemAt(i)
|
||||
for i in range(self.window.engine_options_layout.count()):
|
||||
item = self.window.engine_options_layout.itemAt(i)
|
||||
layout = item.layout() # get the layout
|
||||
for x in range(layout.count()):
|
||||
z = layout.itemAt(x)
|
||||
@@ -497,7 +497,7 @@ class SubmitWorker(QThread):
|
||||
job_list = [job_json]
|
||||
|
||||
# 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)
|
||||
# submit
|
||||
err_msg = ""
|
||||
@@ -93,7 +93,7 @@ class EngineBrowserWindow(QMainWindow):
|
||||
def update_table(self):
|
||||
|
||||
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:
|
||||
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.status_utils import RenderStatus
|
||||
from src.utilities.zeroconf_server import ZeroconfServer
|
||||
from src.ui.add_job import NewRenderJobForm
|
||||
from src.ui.console import ConsoleWindow
|
||||
from src.ui.add_job_window import NewRenderJobForm
|
||||
from src.ui.console_window import ConsoleWindow
|
||||
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.proportional_image_label import ProportionalImageLabel
|
||||
from src.ui.widgets.statusbar import StatusBar
|
||||
@@ -306,12 +306,12 @@ class MainWindow(QMainWindow):
|
||||
get_time_elapsed(start_time, end_time)
|
||||
|
||||
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', ''))
|
||||
total_frames = str(job.get('total_frames', ''))
|
||||
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(total_frames), QTableWidgetItem(date_created_string)]
|
||||
|
||||
@@ -395,7 +395,7 @@ class MainWindow(QMainWindow):
|
||||
return []
|
||||
|
||||
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"])
|
||||
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,
|
||||
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),
|
||||
'-pix_fmt', 'yuva444p10le', output_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
check=True)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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',
|
||||
'ultrafast', '-an', dest_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||
|
||||
Reference in New Issue
Block a user