Merge branch 'develop' into enhancement/farm-env-variables

This commit is contained in:
Jakub Trllo 2024-11-25 14:50:32 +01:00 committed by GitHub
commit 8111b4d47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 165 additions and 98 deletions

View file

@ -535,8 +535,8 @@ class AYONAddon(ABC):
Implementation of this method is optional.
Note:
The logic can be similar to logic in tray, but tray does not require
to be logged in.
The logic can be similar to logic in tray, but tray does not
require to be logged in.
Args:
process_context (ProcessContext): Context of child

View file

@ -146,7 +146,8 @@ def publish_report_viewer():
@main_cli.command()
@click.argument("output_path")
@click.option("--project", help="Define project context")
@click.option("--folder", help="Define folder in project (project must be set)")
@click.option(
"--folder", help="Define folder in project (project must be set)")
@click.option(
"--strict",
is_flag=True,

View file

@ -616,7 +616,9 @@ class EnumDef(AbstractAttrDef):
return data
@staticmethod
def prepare_enum_items(items: "EnumItemsInputType") -> List["EnumItemDict"]:
def prepare_enum_items(
items: "EnumItemsInputType"
) -> List["EnumItemDict"]:
"""Convert items to unified structure.
Output is a list where each item is dictionary with 'value'

View file

@ -276,12 +276,7 @@ class ASettingRegistry(ABC):
@abstractmethod
def _delete_item(self, name):
# type: (str) -> None
"""Delete item from settings.
Note:
see :meth:`ayon_core.lib.user_settings.ARegistrySettings.delete_item`
"""
"""Delete item from settings."""
pass
def __delitem__(self, name):
@ -433,12 +428,7 @@ class IniSettingRegistry(ASettingRegistry):
config.write(cfg)
def _delete_item(self, name):
"""Delete item from default section.
Note:
See :meth:`~ayon_core.lib.IniSettingsRegistry.delete_item_from_section`
"""
"""Delete item from default section."""
self.delete_item_from_section("MAIN", name)

View file

@ -1283,12 +1283,16 @@ class CreateContext:
@contextmanager
def bulk_pre_create_attr_defs_change(self, sender=None):
with self._bulk_context("pre_create_attrs_change", sender) as bulk_info:
with self._bulk_context(
"pre_create_attrs_change", sender
) as bulk_info:
yield bulk_info
@contextmanager
def bulk_create_attr_defs_change(self, sender=None):
with self._bulk_context("create_attrs_change", sender) as bulk_info:
with self._bulk_context(
"create_attrs_change", sender
) as bulk_info:
yield bulk_info
@contextmanager
@ -1946,9 +1950,9 @@ class CreateContext:
creator are just removed from context.
Args:
instances (List[CreatedInstance]): Instances that should be removed.
Remove logic is done using creator, which may require to
do other cleanup than just remove instance from context.
instances (List[CreatedInstance]): Instances that should be
removed. Remove logic is done using creator, which may require
to do other cleanup than just remove instance from context.
sender (Optional[str]): Sender of the event.
"""

View file

@ -1,5 +1,9 @@
import ayon_api
from ayon_core.lib import StringTemplate, filter_profiles, prepare_template_data
from ayon_core.lib import (
StringTemplate,
filter_profiles,
prepare_template_data,
)
from ayon_core.settings import get_project_settings
from .constants import DEFAULT_PRODUCT_TEMPLATE

View file

@ -222,6 +222,9 @@ def remap_range_on_file_sequence(otio_clip, in_out_range):
source_range = otio_clip.source_range
available_range_rate = available_range.start_time.rate
media_in = available_range.start_time.value
available_range_start_frame = (
available_range.start_time.to_frames()
)
# Temporary.
# Some AYON custom OTIO exporter were implemented with relative
@ -230,7 +233,7 @@ def remap_range_on_file_sequence(otio_clip, in_out_range):
# while we are updating those.
if (
is_clip_from_media_sequence(otio_clip)
and otio_clip.available_range().start_time.to_frames() == media_ref.start_frame
and available_range_start_frame == media_ref.start_frame
and source_range.start_time.to_frames() < media_ref.start_frame
):
media_in = 0
@ -303,8 +306,12 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
rounded_av_rate = round(available_range_rate, 2)
rounded_src_rate = round(source_range.start_time.rate, 2)
if rounded_av_rate != rounded_src_rate:
conformed_src_in = source_range.start_time.rescaled_to(available_range_rate)
conformed_src_duration = source_range.duration.rescaled_to(available_range_rate)
conformed_src_in = source_range.start_time.rescaled_to(
available_range_rate
)
conformed_src_duration = source_range.duration.rescaled_to(
available_range_rate
)
conformed_source_range = otio.opentime.TimeRange(
start_time=conformed_src_in,
duration=conformed_src_duration

View file

@ -18,13 +18,13 @@ def parse_ayon_entity_uri(uri: str) -> Optional[dict]:
Example:
>>> parse_ayon_entity_uri(
>>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd" # noqa: E501
>>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd"
>>> )
{'project': 'test', 'folderPath': '/char/villain',
'product': 'modelMain', 'version': 1,
'representation': 'usd'}
>>> parse_ayon_entity_uri(
>>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr" # noqa: E501
>>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr"
>>> )
{'project': 'project', 'folderPath': '/folder',
'product': 'renderMain', 'version': 3,
@ -34,7 +34,7 @@ def parse_ayon_entity_uri(uri: str) -> Optional[dict]:
dict[str, Union[str, int]]: The individual key with their values as
found in the ayon entity URI.
"""
""" # noqa: E501
if not (uri.startswith("ayon+entity://") or uri.startswith("ayon://")):
return {}

View file

@ -8,7 +8,10 @@ import attr
import ayon_api
import clique
from ayon_core.lib import Logger, collect_frames
from ayon_core.pipeline import get_current_project_name, get_representation_path
from ayon_core.pipeline import (
get_current_project_name,
get_representation_path,
)
from ayon_core.pipeline.create import get_product_name
from ayon_core.pipeline.farm.patterning import match_aov_pattern
from ayon_core.pipeline.publish import KnownPublishError
@ -771,9 +774,14 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
project_settings = instance.context.data.get("project_settings")
use_legacy_product_name = True
try:
use_legacy_product_name = project_settings["core"]["tools"]["creator"]["use_legacy_product_names_for_renders"] # noqa: E501
use_legacy_product_name = (
project_settings
["core"]
["tools"]
["creator"]
["use_legacy_product_names_for_renders"]
)
except KeyError:
warnings.warn(
("use_legacy_for_renders not found in project settings. "
@ -789,7 +797,9 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
dynamic_data=dynamic_data)
else:
product_name, group_name = get_product_name_and_group_from_template(
(
product_name, group_name
) = get_product_name_and_group_from_template(
task_entity=instance.data["taskEntity"],
project_name=instance.context.data["projectName"],
host_name=instance.context.data["hostName"],
@ -932,7 +942,7 @@ def _collect_expected_files_for_aov(files):
# but we really expect only one collection.
# Nothing else make sense.
if len(cols) != 1:
raise ValueError("Only one image sequence type is expected.") # noqa: E501
raise ValueError("Only one image sequence type is expected.")
return list(cols[0])

View file

@ -43,7 +43,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
shot_data = {
"entity_type": "folder",
# WARNING unless overwritten, default folder type is hardcoded to shot
# WARNING unless overwritten, default folder type is hardcoded
# to shot
"folder_type": instance.data.get("folder_type") or "Shot",
"tasks": instance.data.get("tasks") or {},
"comments": instance.data.get("comments", []),

View file

@ -129,26 +129,33 @@ class ExtractOTIOReview(
res_data[key] = value
break
self.to_width, self.to_height = res_data["width"], res_data["height"]
self.log.debug("> self.to_width x self.to_height: {} x {}".format(
self.to_width, self.to_height
))
self.to_width, self.to_height = (
res_data["width"], res_data["height"]
)
self.log.debug(
"> self.to_width x self.to_height:"
f" {self.to_width} x {self.to_height}"
)
available_range = r_otio_cl.available_range()
available_range_start_frame = (
available_range.start_time.to_frames()
)
processing_range = None
self.actual_fps = available_range.duration.rate
start = src_range.start_time.rescaled_to(self.actual_fps)
duration = src_range.duration.rescaled_to(self.actual_fps)
src_frame_start = src_range.start_time.to_frames()
# Temporary.
# Some AYON custom OTIO exporter were implemented with relative
# source range for image sequence. Following code maintain
# backward-compatibility by adjusting available range
# Some AYON custom OTIO exporter were implemented with
# relative source range for image sequence. Following code
# maintain backward-compatibility by adjusting available range
# while we are updating those.
if (
is_clip_from_media_sequence(r_otio_cl)
and available_range.start_time.to_frames() == media_ref.start_frame
and src_range.start_time.to_frames() < media_ref.start_frame
and available_range_start_frame == media_ref.start_frame
and src_frame_start < media_ref.start_frame
):
available_range = otio.opentime.TimeRange(
otio.opentime.RationalTime(0, rate=self.actual_fps),
@ -246,7 +253,8 @@ class ExtractOTIOReview(
# Extraction via FFmpeg.
else:
path = media_ref.target_url
# Set extract range from 0 (FFmpeg ignores embedded timecode).
# Set extract range from 0 (FFmpeg ignores
# embedded timecode).
extract_range = otio.opentime.TimeRange(
otio.opentime.RationalTime(
(
@ -414,7 +422,8 @@ class ExtractOTIOReview(
to defined image sequence format.
Args:
sequence (list): input dir path string, collection object, fps in list
sequence (list): input dir path string, collection object,
fps in list.
video (list)[optional]: video_path string, otio_range in list
gap (int)[optional]: gap duration
end_offset (int)[optional]: offset gap frame start in frames

View file

@ -11,8 +11,8 @@ class ValidateProductUniqueness(pyblish.api.ContextPlugin):
"""Validate all product names are unique.
This only validates whether the instances currently set to publish from
the workfile overlap one another for the folder + product they are publishing
to.
the workfile overlap one another for the folder + product they are
publishing to.
This does not perform any check against existing publishes in the database
since it is allowed to publish into existing products resulting in
@ -72,8 +72,10 @@ class ValidateProductUniqueness(pyblish.api.ContextPlugin):
# All is ok
return
msg = ("Instance product names {} are not unique. ".format(non_unique) +
"Please remove or rename duplicates.")
msg = (
f"Instance product names {non_unique} are not unique."
" Please remove or rename duplicates."
)
formatting_data = {
"non_unique": ",".join(non_unique)
}

View file

@ -79,7 +79,8 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
- Datatypes explanation:
<color> string format must be supported by FFmpeg.
Examples: "#000000", "0x000000", "black"
<font> must be accesible by ffmpeg = name of registered Font in system or path to font file.
<font> must be accesible by ffmpeg = name of registered Font in system
or path to font file.
Examples: "Arial", "C:/Windows/Fonts/arial.ttf"
- Possible keys:
@ -87,17 +88,21 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
"bg_opacity" - Opacity of background (box around text) - <float, Range:0-1>
"bg_color" - Background color - <color>
"bg_padding" - Background padding in pixels - <int>
"x_offset" - offsets burnin vertically by entered pixels from border - <int>
"y_offset" - offsets burnin horizontally by entered pixels from border - <int>
"x_offset" - offsets burnin vertically by entered pixels
from border - <int>
"y_offset" - offsets burnin horizontally by entered pixels
from border - <int>
- x_offset & y_offset should be set at least to same value as bg_padding!!
"font" - Font Family for text - <font>
"font_size" - Font size in pixels - <int>
"font_color" - Color of text - <color>
"frame_offset" - Default start frame - <int>
- required IF start frame is not set when using frames or timecode burnins
- required IF start frame is not set when using frames
or timecode burnins
On initializing class can be set General options through "options_init" arg.
General can be overridden when adding burnin
On initializing class can be set General options through
"options_init" arg.
General options can be overridden when adding burnin.
'''
TOP_CENTERED = ffmpeg_burnins.TOP_CENTERED

View file

@ -190,6 +190,7 @@ def get_current_project_settings():
project_name = os.environ.get("AYON_PROJECT_NAME")
if not project_name:
raise ValueError(
"Missing context project in environemt variable `AYON_PROJECT_NAME`."
"Missing context project in environment"
" variable `AYON_PROJECT_NAME`."
)
return get_project_settings(project_name)

View file

@ -217,7 +217,9 @@ class ProductTypeDescriptionWidget(QtWidgets.QWidget):
product_type_label = QtWidgets.QLabel(self)
product_type_label.setObjectName("CreatorProductTypeLabel")
product_type_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)
product_type_label.setAlignment(
QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft
)
help_label = QtWidgets.QLabel(self)
help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)

View file

@ -21,9 +21,9 @@ except ImportError:
Application action based on 'ApplicationManager' system.
Handling of applications in launcher is not ideal and should be completely
redone from scratch. This is just a temporary solution to keep backwards
compatibility with AYON launcher.
Handling of applications in launcher is not ideal and should be
completely redone from scratch. This is just a temporary solution
to keep backwards compatibility with AYON launcher.
Todos:
Move handling of errors to frontend.

View file

@ -517,7 +517,11 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
def setItemCheckState(self, index, state):
self.setItemData(index, state, QtCore.Qt.CheckStateRole)
def set_value(self, values: Optional[Iterable[Any]], role: Optional[int] = None):
def set_value(
self,
values: Optional[Iterable[Any]],
role: Optional[int] = None,
):
if role is None:
role = self._value_role

View file

@ -499,8 +499,10 @@ class ProductsModel(QtGui.QStandardItemModel):
version_item.version_id
for version_item in last_version_by_product_id.values()
}
repre_count_by_version_id = self._controller.get_versions_representation_count(
project_name, version_ids
repre_count_by_version_id = (
self._controller.get_versions_representation_count(
project_name, version_ids
)
)
sync_availability_by_version_id = (
self._controller.get_version_sync_availability(

View file

@ -339,7 +339,9 @@ class OverviewWidget(QtWidgets.QFrame):
self._change_visibility_for_state()
self._product_content_layout.addWidget(self._create_widget, 7)
self._product_content_layout.addWidget(self._product_views_widget, 3)
self._product_content_layout.addWidget(self._product_attributes_wrap, 7)
self._product_content_layout.addWidget(
self._product_attributes_wrap, 7
)
def _change_visibility_for_state(self):
self._create_widget.setVisible(

View file

@ -214,8 +214,8 @@ class TasksCombobox(QtWidgets.QComboBox):
Combobox gives ability to select only from intersection of task names for
folder paths in selected instances.
If folder paths in selected instances does not have same tasks then combobox
will be empty.
If folder paths in selected instances does not have same tasks
then combobox will be empty.
"""
value_changed = QtCore.Signal()
@ -604,7 +604,7 @@ class VariantInputWidget(PlaceholderLineEdit):
class GlobalAttrsWidget(QtWidgets.QWidget):
"""Global attributes mainly to define context and product name of instances.
"""Global attributes to define context and product name of instances.
product name is or may be affected on context. Gives abiity to modify
context and product name of instance. This change is not autopromoted but

View file

@ -22,8 +22,8 @@ class TasksModel(QtGui.QStandardItemModel):
tasks with same names then model is empty too.
Args:
controller (AbstractPublisherFrontend): Controller which handles creation and
publishing.
controller (AbstractPublisherFrontend): Controller which handles
creation and publishing.
"""
def __init__(

View file

@ -998,7 +998,11 @@ class PublisherWindow(QtWidgets.QDialog):
new_item["label"] = new_item.pop("creator_label")
new_item["identifier"] = new_item.pop("creator_identifier")
new_failed_info.append(new_item)
self.add_error_message_dialog(event["title"], new_failed_info, "Creator:")
self.add_error_message_dialog(
event["title"],
new_failed_info,
"Creator:"
)
def _on_convertor_error(self, event):
new_failed_info = []

View file

@ -366,8 +366,8 @@ class ContainersModel:
try:
uuid.UUID(repre_id)
except (ValueError, TypeError, AttributeError):
# Fake not existing representation id so container is shown in UI
# but as invalid
# Fake not existing representation id so container
# is shown in UI but as invalid
item.representation_id = invalid_ids_mapping.setdefault(
repre_id, uuid.uuid4().hex
)

View file

@ -556,9 +556,10 @@ class _IconsCache:
log.info("Didn't find icon \"{}\"".format(icon_name))
elif used_variant != icon_name:
log.debug("Icon \"{}\" was not found \"{}\" is used instead".format(
icon_name, used_variant
))
log.debug(
f"Icon \"{icon_name}\" was not found"
f" \"{used_variant}\" is used instead"
)
cls._qtawesome_cache[full_icon_name] = icon
return icon

View file

@ -68,7 +68,7 @@ target-version = "py39"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F", "W"]
select = ["E", "F", "W"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.

View file

@ -358,7 +358,10 @@ class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
custom_tags: list[str] = SettingsField(
default_factory=list,
title="Custom Tags",
description="Additional custom tags that will be added to the created representation."
description=(
"Additional custom tags that will be added"
" to the created representation."
)
)
@ -892,9 +895,11 @@ class PublishPuginsModel(BaseSettingsModel):
default_factory=CollectFramesFixDefModel,
title="Collect Frames to Fix",
)
CollectUSDLayerContributions: CollectUSDLayerContributionsModel = SettingsField(
default_factory=CollectUSDLayerContributionsModel,
title="Collect USD Layer Contributions",
CollectUSDLayerContributions: CollectUSDLayerContributionsModel = (
SettingsField(
default_factory=CollectUSDLayerContributionsModel,
title="Collect USD Layer Contributions",
)
)
ValidateEditorialAssetName: ValidateBaseModel = SettingsField(
default_factory=ValidateBaseModel,
@ -1214,7 +1219,9 @@ DEFAULT_PUBLISH_VALUES = {
"TOP_RIGHT": "{anatomy[version]}",
"BOTTOM_LEFT": "{username}",
"BOTTOM_CENTERED": "{folder[name]}",
"BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}",
"BOTTOM_RIGHT": (
"{frame_start}-{current_frame}-{frame_end}"
),
"filter": {
"families": [],
"tags": []
@ -1240,7 +1247,9 @@ DEFAULT_PUBLISH_VALUES = {
"TOP_RIGHT": "{anatomy[version]}",
"BOTTOM_LEFT": "{username}",
"BOTTOM_CENTERED": "{folder[name]}",
"BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}",
"BOTTOM_RIGHT": (
"{frame_start}-{current_frame}-{frame_end}"
),
"filter": {
"families": [],
"tags": []

View file

@ -83,8 +83,8 @@ class CreatorToolModel(BaseSettingsModel):
filter_creator_profiles: list[FilterCreatorProfile] = SettingsField(
default_factory=list,
title="Filter creator profiles",
description="Allowed list of creator labels that will be only shown if "
"profile matches context."
description="Allowed list of creator labels that will be only shown"
" if profile matches context."
)
@validator("product_types_smart_select")
@ -426,7 +426,9 @@ DEFAULT_TOOLS_VALUES = {
],
"task_types": [],
"tasks": [],
"template": "{product[type]}{Task[name]}_{Renderlayer}_{Renderpass}"
"template": (
"{product[type]}{Task[name]}_{Renderlayer}_{Renderpass}"
)
},
{
"product_types": [

View file

@ -130,19 +130,20 @@ def test_image_sequence_and_handles_out_of_range():
expected = [
# 5 head black frames generated from gap (991-995)
"/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune "
"stillimage -start_number 991 C:/result/output.%03d.jpg",
"/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720"
" -tune stillimage -start_number 991 C:/result/output.%03d.jpg",
# 9 tail back frames generated from gap (1097-1105)
"/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune "
"stillimage -start_number 1097 C:/result/output.%03d.jpg",
"/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720"
" -tune stillimage -start_number 1097 C:/result/output.%03d.jpg",
# Report from source tiff (996-1096)
# 996-1000 = additional 5 head frames
# 1001-1095 = source range conformed to 25fps
# 1096-1096 = additional 1 tail frames
"/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i "
f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996 C:/result/output.%03d.jpg"
f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996"
f" C:/result/output.%03d.jpg"
]
assert calls == expected
@ -179,13 +180,13 @@ def test_short_movie_head_gap_handles():
expected = [
# 10 head black frames generated from gap (991-1000)
"/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune "
"stillimage -start_number 991 C:/result/output.%03d.jpg",
"/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720"
" -tune stillimage -start_number 991 C:/result/output.%03d.jpg",
# source range + 10 tail frames
# duration = 50fr (source) + 10fr (tail handle) = 60 fr = 2.4s
"/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4 -start_number 1001 "
"C:/result/output.%03d.jpg"
"/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4"
" -start_number 1001 C:/result/output.%03d.jpg"
]
assert calls == expected
@ -208,7 +209,8 @@ def test_short_movie_tail_gap_handles():
# 10 head frames + source range
# duration = 10fr (head handle) + 66fr (source) = 76fr = 3.16s
"/path/to/ffmpeg -ss 1.0416666666666667 -t 3.1666666666666665 -i "
"C:\\data\\qt_no_tc_24fps.mov -start_number 991 C:/result/output.%03d.jpg"
"C:\\data\\qt_no_tc_24fps.mov -start_number 991"
" C:/result/output.%03d.jpg"
]
assert calls == expected
@ -234,10 +236,12 @@ def test_multiple_review_clips_no_gap():
expected = [
# 10 head black frames generated from gap (991-1000)
'/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune '
'/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi'
' -i color=c=black:s=1280x720 -tune '
'stillimage -start_number 991 C:/result/output.%03d.jpg',
# Alternance 25fps tiff sequence and 24fps exr sequence for 100 frames each
# Alternance 25fps tiff sequence and 24fps exr sequence
# for 100 frames each
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif '
'-start_number 1001 C:/result/output.%03d.jpg',
@ -315,7 +319,8 @@ def test_multiple_review_clips_with_gap():
expected = [
# Gap on review track (12 frames)
'/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi -i color=c=black:s=1280x720 -tune '
'/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi'
' -i color=c=black:s=1280x720 -tune '
'stillimage -start_number 991 C:/result/output.%03d.jpg',
'/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '