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
|
uses: sayyid5416/pyinstaller@v1
|
||||||
with:
|
with:
|
||||||
python_ver: '3.11'
|
python_ver: '3.11'
|
||||||
spec: 'main.spec'
|
spec: 'client.spec'
|
||||||
requirements: 'requirements.txt'
|
requirements: 'requirements.txt'
|
||||||
upload_exe_with_name: 'Zordon'
|
upload_exe_with_name: 'Zordon'
|
||||||
pyinstaller-build-linux:
|
pyinstaller-build-linux:
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
uses: sayyid5416/pyinstaller@v1
|
uses: sayyid5416/pyinstaller@v1
|
||||||
with:
|
with:
|
||||||
python_ver: '3.11'
|
python_ver: '3.11'
|
||||||
spec: 'main.spec'
|
spec: 'client.spec'
|
||||||
requirements: 'requirements.txt'
|
requirements: 'requirements.txt'
|
||||||
upload_exe_with_name: 'Zordon'
|
upload_exe_with_name: 'Zordon'
|
||||||
pyinstaller-build-macos:
|
pyinstaller-build-macos:
|
||||||
@@ -33,6 +33,6 @@ jobs:
|
|||||||
uses: sayyid5416/pyinstaller@v1
|
uses: sayyid5416/pyinstaller@v1
|
||||||
with:
|
with:
|
||||||
python_ver: '3.11'
|
python_ver: '3.11'
|
||||||
spec: 'main.spec'
|
spec: 'client.spec'
|
||||||
requirements: 'requirements.txt'
|
requirements: 'requirements.txt'
|
||||||
upload_exe_with_name: 'Zordon'
|
upload_exe_with_name: 'Zordon'
|
||||||
|
|||||||
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()
|
||||||
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(
|
a = Analysis(
|
||||||
['main.py'],
|
['client.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=binaries,
|
binaries=binaries,
|
||||||
datas=datas,
|
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
|
import logging
|
||||||
from src.init import run
|
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__':
|
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)
|
uploaded_project = request.files.get('file', None)
|
||||||
project_url = jobs_list[0].get('url', None)
|
project_url = jobs_list[0].get('url', None)
|
||||||
local_path = jobs_list[0].get('local_path', 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
|
downloaded_file_url = None
|
||||||
|
|
||||||
if uploaded_project and uploaded_project.filename:
|
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
|
# 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)]
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ ssl._create_default_https_context = ssl._create_unverified_context # disable SS
|
|||||||
|
|
||||||
API_VERSION = "1"
|
API_VERSION = "1"
|
||||||
|
|
||||||
def start_server(hostname=None):
|
def start_api_server(hostname=None):
|
||||||
|
|
||||||
# get hostname
|
# get hostname
|
||||||
if not hostname:
|
if not hostname:
|
||||||
@@ -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,27 +442,31 @@ 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
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# Miscellaneous:
|
# Miscellaneous:
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
@server.get('/api/heartbeat')
|
||||||
|
def heartbeat():
|
||||||
|
return datetime.now().isoformat(), 200
|
||||||
|
|
||||||
@server.post('/api/job/<job_id>/send_subjob_update_notification')
|
@server.post('/api/job/<job_id>/send_subjob_update_notification')
|
||||||
def subjob_update_notification(job_id):
|
def subjob_update_notification(job_id):
|
||||||
subjob_details = request.json
|
subjob_details = request.json
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ categories = [RenderStatus.RUNNING, RenderStatus.WAITING_FOR_SUBJOBS, RenderStat
|
|||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
OFFLINE_MAX = 4
|
OFFLINE_MAX = 4
|
||||||
LOOPBACK = '127.0.0.1'
|
|
||||||
|
|
||||||
|
|
||||||
class RenderServerProxy:
|
class RenderServerProxy:
|
||||||
@@ -55,14 +54,18 @@ class RenderServerProxy:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<RenderServerProxy - {self.hostname}>"
|
return f"<RenderServerProxy - {self.hostname}>"
|
||||||
|
|
||||||
def connect(self):
|
def check_connection(self):
|
||||||
return self.status()
|
try:
|
||||||
|
return self.request("heartbeat").ok
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def is_online(self):
|
def is_online(self):
|
||||||
if self.__update_in_background:
|
if self.__update_in_background:
|
||||||
return self.__offline_flags < OFFLINE_MAX
|
return self.__offline_flags < OFFLINE_MAX
|
||||||
else:
|
else:
|
||||||
return self.get_status() is not None
|
return self.check_connection()
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
if not self.is_online():
|
if not self.is_online():
|
||||||
@@ -102,8 +105,7 @@ class RenderServerProxy:
|
|||||||
|
|
||||||
def request(self, payload, timeout=5):
|
def request(self, payload, timeout=5):
|
||||||
from src.api.api_server import API_VERSION
|
from src.api.api_server import API_VERSION
|
||||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout,
|
||||||
return requests.get(f'http://{hostname}:{self.port}/api/{payload}', timeout=timeout,
|
|
||||||
headers={"X-API-Version": str(API_VERSION)})
|
headers={"X-API-Version": str(API_VERSION)})
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
@@ -203,7 +205,7 @@ class RenderServerProxy:
|
|||||||
if self.is_localhost:
|
if self.is_localhost:
|
||||||
jobs_with_path = [{'local_path': file_path, **item} for item in job_list]
|
jobs_with_path = [{'local_path': file_path, **item} for item in job_list]
|
||||||
job_data = json.dumps(jobs_with_path)
|
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'}
|
headers = {'Content-Type': 'application/json'}
|
||||||
return requests.post(url, data=job_data, headers=headers)
|
return requests.post(url, data=job_data, headers=headers)
|
||||||
|
|
||||||
@@ -245,32 +247,32 @@ class RenderServerProxy:
|
|||||||
Returns:
|
Returns:
|
||||||
Response: The response from the server.
|
Response: The response from the server.
|
||||||
"""
|
"""
|
||||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
return requests.post(f'http://{self.hostname}:{self.port}/api/job/{parent_id}/send_subjob_update_notification',
|
||||||
return requests.post(f'http://{hostname}:{self.port}/api/job/{parent_id}/send_subjob_update_notification',
|
|
||||||
json=subjob.json())
|
json=subjob.json())
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# Renderers:
|
# Engines:
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
def is_engine_available(self, engine_name):
|
def is_engine_available(self, engine_name):
|
||||||
return self.request_data(f'{engine_name}/is_available')
|
return self.request_data(f'{engine_name}/is_available')
|
||||||
|
|
||||||
def get_all_engines(self):
|
def get_all_engines(self):
|
||||||
|
# todo: this doesnt work
|
||||||
return self.request_data('all_engines')
|
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:
|
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.
|
timeout (int, optional): The number of seconds to wait for a response from the server. Defaults to 5.
|
||||||
|
|
||||||
Returns:
|
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
|
return all_data
|
||||||
|
|
||||||
def delete_engine(self, engine, version, system_cpu=None):
|
def delete_engine(self, engine, version, system_cpu=None):
|
||||||
@@ -286,21 +288,18 @@ class RenderServerProxy:
|
|||||||
Response: The response from the server.
|
Response: The response from the server.
|
||||||
"""
|
"""
|
||||||
form_data = {'engine': engine, 'version': version, 'system_cpu': system_cpu}
|
form_data = {'engine': engine, 'version': version, 'system_cpu': system_cpu}
|
||||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
return requests.post(f'http://{self.hostname}:{self.port}/api/delete_engine', json=form_data)
|
||||||
return requests.post(f'http://{hostname}:{self.port}/api/delete_engine', json=form_data)
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# Download Files:
|
# Download Files:
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
def download_all_job_files(self, job_id, save_path):
|
def download_all_job_files(self, job_id, save_path):
|
||||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
url = f"http://{self.hostname}:{self.port}/api/job/{job_id}/download_all"
|
||||||
url = f"http://{hostname}:{self.port}/api/job/{job_id}/download_all"
|
|
||||||
return self.__download_file_from_url(url, output_filepath=save_path)
|
return self.__download_file_from_url(url, output_filepath=save_path)
|
||||||
|
|
||||||
def download_job_file(self, job_id, job_filename, 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://{self.hostname}:{self.port}/api/job/{job_id}/download?filename={job_filename}"
|
||||||
url = f"http://{hostname}:{self.port}/api/job/{job_id}/download?filename={job_filename}"
|
|
||||||
return self.__download_file_from_url(url, output_filepath=save_path)
|
return self.__download_file_from_url(url, output_filepath=save_path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
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:
|
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