mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 08:48:13 +00:00
Blender image sequences now generate a preview mp4 on completion
This commit is contained in:
@@ -7,8 +7,7 @@ import requests
|
|||||||
from sqlalchemy import create_engine, Column, String, Integer
|
from sqlalchemy import create_engine, Column, String, Integer
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from .render_workers.base_worker import RenderStatus
|
from .render_workers.base_worker import RenderStatus, BaseRenderWorker, Base
|
||||||
from .scheduled_job import ScheduledJob, Base
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -45,7 +44,6 @@ class RenderQueue:
|
|||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
Session = sessionmaker(bind=engine)
|
Session = sessionmaker(bind=engine)
|
||||||
session = Session()
|
session = Session()
|
||||||
ScheduledJob.register_user_events()
|
|
||||||
job_queue = []
|
job_queue = []
|
||||||
maximum_renderer_instances = {'blender': 1, 'aerender': 1, 'ffmpeg': 4}
|
maximum_renderer_instances = {'blender': 1, 'aerender': 1, 'ffmpeg': 4}
|
||||||
hostname = None
|
hostname = None
|
||||||
@@ -62,7 +60,7 @@ class RenderQueue:
|
|||||||
def add_to_render_queue(cls, render_job, force_start=False, client=None):
|
def add_to_render_queue(cls, render_job, force_start=False, client=None):
|
||||||
|
|
||||||
if not client or render_job.client == cls.hostname:
|
if not client or render_job.client == cls.hostname:
|
||||||
logger.debug('Adding priority {} job to render queue: {}'.format(render_job.priority, render_job.worker_object))
|
logger.debug('Adding priority {} job to render queue: {}'.format(render_job.priority, render_job))
|
||||||
render_job.client = cls.hostname
|
render_job.client = cls.hostname
|
||||||
cls.job_queue.append(render_job)
|
cls.job_queue.append(render_job)
|
||||||
if force_start:
|
if force_start:
|
||||||
@@ -89,7 +87,7 @@ class RenderQueue:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def jobs_with_status(cls, status, priority_sorted=False):
|
def jobs_with_status(cls, status, priority_sorted=False):
|
||||||
found_jobs = [x for x in cls.all_jobs() if x.render_status() == status]
|
found_jobs = [x for x in cls.all_jobs() if x.status == status]
|
||||||
if priority_sorted:
|
if priority_sorted:
|
||||||
found_jobs = sorted(found_jobs, key=lambda a: a.priority, reverse=False)
|
found_jobs = sorted(found_jobs, key=lambda a: a.priority, reverse=False)
|
||||||
return found_jobs
|
return found_jobs
|
||||||
@@ -103,7 +101,7 @@ class RenderQueue:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_history(cls):
|
def clear_history(cls):
|
||||||
to_remove = [x for x in cls.all_jobs() if x.render_status() in [RenderStatus.CANCELLED,
|
to_remove = [x for x in cls.all_jobs() if x.status in [RenderStatus.CANCELLED,
|
||||||
RenderStatus.COMPLETED, RenderStatus.ERROR]]
|
RenderStatus.COMPLETED, RenderStatus.ERROR]]
|
||||||
for job_to_remove in to_remove:
|
for job_to_remove in to_remove:
|
||||||
cls.delete_job(job_to_remove)
|
cls.delete_job(job_to_remove)
|
||||||
@@ -141,15 +139,14 @@ class RenderQueue:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start_job(cls, job):
|
def start_job(cls, job):
|
||||||
logger.info('Starting {}render: {} - Priority {}'.format('scheduled ' if job.scheduled_start else '', job.name,
|
logger.info(f'Starting render: {job.name} - Priority {job.priority}')
|
||||||
job.priority))
|
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cancel_job(cls, job):
|
def cancel_job(cls, job):
|
||||||
logger.info(f'Cancelling job ID: {job.id}')
|
logger.info(f'Cancelling job ID: {job.id}')
|
||||||
job.stop()
|
job.stop()
|
||||||
return job.render_status() == RenderStatus.CANCELLED
|
return job.status == RenderStatus.CANCELLED
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_job(cls, job):
|
def delete_job(cls, job):
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ class AERenderWorker(BaseRenderWorker):
|
|||||||
supported_extensions = ['.aep']
|
supported_extensions = ['.aep']
|
||||||
engine = AERender
|
engine = AERender
|
||||||
|
|
||||||
def __init__(self, input_path, output_path, args=None):
|
def __init__(self, input_path, output_path, priority=2, args=None, owner=None,
|
||||||
super(AERenderWorker, self).__init__(input_path=input_path, output_path=output_path, ignore_extensions=False,
|
client=None, name=None):
|
||||||
args=args)
|
super(AERenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
|
||||||
|
client=client, priority=priority, owner=owner, name=name)
|
||||||
|
|
||||||
self.comp = args.get('comp', None)
|
self.comp = args.get('comp', None)
|
||||||
self.render_settings = args.get('render_settings', None)
|
self.render_settings = args.get('render_settings', None)
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime, JSON, event
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class RenderStatus(Enum):
|
class RenderStatus(Enum):
|
||||||
@@ -30,11 +34,25 @@ def string_to_status(string):
|
|||||||
return RenderStatus.ERROR
|
return RenderStatus.ERROR
|
||||||
|
|
||||||
|
|
||||||
class BaseRenderWorker(object):
|
class BaseRenderWorker(Base):
|
||||||
|
__tablename__ = 'render_workers'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
input_path = Column(String)
|
||||||
|
output_path = Column(String)
|
||||||
|
date_created = Column(DateTime)
|
||||||
|
renderer = Column(String)
|
||||||
|
renderer_version = Column(String)
|
||||||
|
priority = Column(Integer)
|
||||||
|
owner = Column(String)
|
||||||
|
client = Column(String)
|
||||||
|
name = Column(String)
|
||||||
|
file_hash = Column(String)
|
||||||
|
|
||||||
engine = None
|
engine = None
|
||||||
|
|
||||||
def __init__(self, input_path, output_path, args=None, ignore_extensions=True):
|
def __init__(self, input_path, output_path, priority=2, args=None, ignore_extensions=True, owner=None, client=None,
|
||||||
|
name=None):
|
||||||
|
|
||||||
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):
|
||||||
@@ -49,7 +67,12 @@ class BaseRenderWorker(object):
|
|||||||
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.renderer_version = self.engine.version()
|
self.renderer_version = self.engine.version()
|
||||||
|
self.priority = priority
|
||||||
|
self.owner = owner
|
||||||
|
self.client = client
|
||||||
|
self.name = name
|
||||||
|
|
||||||
# Frame Ranges
|
# Frame Ranges
|
||||||
self.total_frames = 0
|
self.total_frames = 0
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
|
|
||||||
engine = Blender
|
engine = Blender
|
||||||
|
|
||||||
def __init__(self, input_path, output_path, args=None):
|
def __init__(self, input_path, output_path, priority=2, args=None, owner=None,
|
||||||
super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path,
|
client=None, name=None):
|
||||||
ignore_extensions=False, args=args)
|
super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
|
||||||
|
client=client, priority=priority, owner=owner, name=name)
|
||||||
|
|
||||||
# Args
|
# Args
|
||||||
self.blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper()
|
self.blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper()
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ class FFMPEGRenderWorker(BaseRenderWorker):
|
|||||||
|
|
||||||
engine = FFMPEG
|
engine = FFMPEG
|
||||||
|
|
||||||
def __init__(self, input_path, output_path, args=None):
|
def __init__(self, input_path, output_path, priority=2, args=None, owner=None,
|
||||||
super(FFMPEGRenderWorker, self).__init__(input_path=input_path, output_path=output_path, ignore_extensions=True,
|
client=None, name=None):
|
||||||
args=args)
|
super(FFMPEGRenderWorker, self).__init__(input_path=input_path, output_path=output_path, args=args,
|
||||||
|
client=client, priority=priority, owner=owner, name=name)
|
||||||
|
|
||||||
stream_info = subprocess.check_output([self.engine.renderer_path(), "-i", # https://stackoverflow.com/a/61604105
|
stream_info = subprocess.check_output([self.engine.renderer_path(), "-i", # https://stackoverflow.com/a/61604105
|
||||||
input_path, "-map", "0:v:0", "-c", "copy", "-f", "null", "-y",
|
input_path, "-map", "0:v:0", "-c", "copy", "-f", "null", "-y",
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ class RenderWorkerFactory:
|
|||||||
return classes
|
return classes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_worker(renderer, input_path, output_path, args=None):
|
def create_worker(renderer, input_path, output_path, priority=2, args=None, owner=None,
|
||||||
|
client=None, name=None):
|
||||||
worker_class = RenderWorkerFactory.class_for_name(renderer)
|
worker_class = RenderWorkerFactory.class_for_name(renderer)
|
||||||
return worker_class(input_path=input_path, output_path=output_path, args=args)
|
return worker_class(input_path=input_path, output_path=output_path, args=args, priority=priority, owner=owner,
|
||||||
|
client=client, name=name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def supported_renderers():
|
def supported_renderers():
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def sorted_jobs(all_jobs, sort_by_date=True):
|
|||||||
sorted_job_list = []
|
sorted_job_list = []
|
||||||
if all_jobs:
|
if all_jobs:
|
||||||
for status_category in categories:
|
for status_category in categories:
|
||||||
found_jobs = [x for x in all_jobs if x.render_status() == status_category.value]
|
found_jobs = [x for x in all_jobs if x.status == status_category.value]
|
||||||
if found_jobs:
|
if found_jobs:
|
||||||
sorted_found_jobs = sorted(found_jobs, key=lambda d: d.date_created, reverse=True)
|
sorted_found_jobs = sorted(found_jobs, key=lambda d: d.date_created, reverse=True)
|
||||||
sorted_job_list.extend(sorted_found_jobs)
|
sorted_job_list.extend(sorted_found_jobs)
|
||||||
@@ -61,11 +61,11 @@ def job_detail(job_id):
|
|||||||
table_html = json2html.json2html.convert(json=found_job.json(),
|
table_html = json2html.json2html.convert(json=found_job.json(),
|
||||||
table_attributes='class="table is-narrow is-striped is-fullwidth"')
|
table_attributes='class="table is-narrow is-striped is-fullwidth"')
|
||||||
media_url = None
|
media_url = None
|
||||||
if found_job.file_list() and found_job.render_status() == RenderStatus.COMPLETED:
|
if found_job.file_list() and found_job.status == RenderStatus.COMPLETED:
|
||||||
media_basename = os.path.basename(found_job.file_list()[0])
|
media_basename = os.path.basename(found_job.file_list()[0])
|
||||||
media_url = f"/api/job/{job_id}/file/{media_basename}"
|
media_url = f"/api/job/{job_id}/file/{media_basename}"
|
||||||
return render_template('details.html', detail_table=table_html, media_url=media_url,
|
return render_template('details.html', detail_table=table_html, media_url=media_url,
|
||||||
hostname=RenderQueue.hostname, job_status=found_job.render_status().value.title(),
|
hostname=RenderQueue.hostname, job_status=found_job.status.value.title(),
|
||||||
job=found_job, renderer_info=renderer_info())
|
job=found_job, renderer_info=renderer_info())
|
||||||
|
|
||||||
|
|
||||||
@@ -79,20 +79,20 @@ def job_thumbnail(job_id):
|
|||||||
thumb_image_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.jpg')
|
thumb_image_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.jpg')
|
||||||
|
|
||||||
if not os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS') and \
|
if not os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS') and \
|
||||||
found_job.render_status() not in [RenderStatus.CANCELLED, RenderStatus.ERROR]:
|
found_job.status not in [RenderStatus.CANCELLED, RenderStatus.ERROR]:
|
||||||
generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=240)
|
generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=240)
|
||||||
|
|
||||||
if os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'):
|
if os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'):
|
||||||
return send_file(thumb_video_path, mimetype="video/mp4")
|
return send_file(thumb_video_path, mimetype="video/mp4")
|
||||||
elif os.path.exists(thumb_image_path):
|
elif os.path.exists(thumb_image_path):
|
||||||
return send_file(thumb_image_path, mimetype='image/jpeg')
|
return send_file(thumb_image_path, mimetype='image/jpeg')
|
||||||
elif found_job.render_status() == RenderStatus.RUNNING:
|
elif found_job.status == RenderStatus.RUNNING:
|
||||||
return send_file('static/images/gears.png', mimetype="image/png")
|
return send_file('static/images/gears.png', mimetype="image/png")
|
||||||
elif found_job.render_status() == RenderStatus.CANCELLED:
|
elif found_job.status == RenderStatus.CANCELLED:
|
||||||
return send_file('static/images/cancelled.png', mimetype="image/png")
|
return send_file('static/images/cancelled.png', mimetype="image/png")
|
||||||
elif found_job.render_status() == RenderStatus.SCHEDULED:
|
elif found_job.status == RenderStatus.SCHEDULED:
|
||||||
return send_file('static/images/scheduled.png', mimetype="image/png")
|
return send_file('static/images/scheduled.png', mimetype="image/png")
|
||||||
elif found_job.render_status() == RenderStatus.NOT_STARTED:
|
elif found_job.status == RenderStatus.NOT_STARTED:
|
||||||
return send_file('static/images/not_started.png', mimetype="image/png")
|
return send_file('static/images/not_started.png', mimetype="image/png")
|
||||||
return send_file('static/images/error.png', mimetype="image/png")
|
return send_file('static/images/error.png', mimetype="image/png")
|
||||||
|
|
||||||
@@ -329,8 +329,13 @@ def add_job(job_params, remove_job_dir_on_failure=False):
|
|||||||
if client == RenderQueue.hostname:
|
if client == RenderQueue.hostname:
|
||||||
logger.info(f"Creating job locally - {name if name else input_path}")
|
logger.info(f"Creating job locally - {name if name else input_path}")
|
||||||
try:
|
try:
|
||||||
render_job = ScheduledJob(renderer, input_path, output_path, args, priority, job_owner, client,
|
render_job = RenderWorkerFactory.create_worker(renderer=renderer, input_path=input_path,
|
||||||
notify=False, custom_id=custom_id, name=name)
|
output_path=output_path, args=args)
|
||||||
|
render_job.client = client
|
||||||
|
render_job.owner = job_owner
|
||||||
|
render_job.name = name
|
||||||
|
render_job.priority = priority
|
||||||
|
|
||||||
RenderQueue.add_to_render_queue(render_job, force_start=force_start)
|
RenderQueue.add_to_render_queue(render_job, force_start=force_start)
|
||||||
return render_job.json()
|
return render_job.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<br>Time Elapsed: <span id="time-elapsed">{{job.worker_data()['time_elapsed']}}</span>
|
<br>Time Elapsed: <span id="time-elapsed">{{job.worker_data()['time_elapsed']}}</span>
|
||||||
</span>
|
</span>
|
||||||
<script>
|
<script>
|
||||||
var startingStatus = '{{job.render_status().value}}';
|
var startingStatus = '{{job.status.value}}';
|
||||||
function update_job() {
|
function update_job() {
|
||||||
$.getJSON('/api/job/{{job.id}}', function(data) {
|
$.getJSON('/api/job/{{job.id}}', function(data) {
|
||||||
document.getElementById('progress-bar').value = (data.percent_complete * 100);
|
document.getElementById('progress-bar').value = (data.percent_complete * 100);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def generate_thumbnail_for_job(job, thumb_video_path, thumb_image_path, max_widt
|
|||||||
os.remove(in_progress_path)
|
os.remove(in_progress_path)
|
||||||
|
|
||||||
# Determine best source file to use for thumbs
|
# Determine best source file to use for thumbs
|
||||||
if job.render_status() == RenderStatus.COMPLETED: # use finished file for thumb
|
if job.status == RenderStatus.COMPLETED: # use finished file for thumb
|
||||||
source_path = job.file_list()
|
source_path = job.file_list()
|
||||||
else:
|
else:
|
||||||
source_path = [job.input_path] # use source if nothing else
|
source_path = [job.input_path] # use source if nothing else
|
||||||
|
|||||||
Reference in New Issue
Block a user