diff --git a/__init__.py b/__init__.py index 4e45700..aa3cb3c 100644 --- a/__init__.py +++ b/__init__.py @@ -36,8 +36,8 @@ if 'bpy' in locals(): import bpy def register(): - print('Register update script handler') bpy.app.handlers.frame_change_post.append(update_text_strips) + bpy.app.handlers.render_pre.append(update_text_strips) if bpy.app.background: return @@ -51,6 +51,7 @@ def register(): def unregister(): bpy.app.handlers.frame_change_post.remove(update_text_strips) + bpy.app.handlers.render_pre.remove(update_text_strips) try: bpy.utils.previews.remove(ASSET_PREVIEWS) diff --git a/bl_utils.py b/bl_utils.py index 7612902..9262d8c 100644 --- a/bl_utils.py +++ b/bl_utils.py @@ -2,9 +2,19 @@ """ Generic Blender functions """ +import os +from pathlib import Path +import subprocess +import json +from textwrap import dedent import bpy + +def abspath(path): + path = os.path.abspath(bpy.path.abspath(path)) + return Path(path) + def get_scene_settings(): return bpy.context.scene.vsetb_settings @@ -17,7 +27,27 @@ def get_strip_settings(): return strip.vsetb_strip_settings -def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs): +def norm_arg(arg_name): + return "--" + arg_name.replace(' ', '-') + +def norm_value(value): + if isinstance(value, (tuple, list)): + values = [] + for v in value: + if not isinstance(v, str): + v = json.dumps(v) + values.append(v) + + return values + + if isinstance(value, Path): + return str(value) + + if not isinstance(value, str): + value = json.dumps(value) + return value + +def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, output=None, **kargs): cmd = [str(blender)] if blender else [bpy.app.binary_path] if background: @@ -31,6 +61,9 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip if blendfile: cmd += [str(blendfile)] + + if output: + cmd += ['-o', str(output)] if script: cmd += ['--python', str(script)] @@ -49,6 +82,28 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip return cmd + +def background_render(output=None): + #bpy.context.scene.render.filepath = '{output}' + script_code = dedent(f""" + import bpy + bpy.context.scene.render.filepath = '{str(output)}' + bpy.ops.render.render(animation=True) + bpy.ops.wm.quit_blender() + """) + + tmp_blend = Path(bpy.app.tempdir) / Path(bpy.data.filepath).name + bpy.ops.wm.save_as_mainfile(filepath=str(tmp_blend)) + + script_path = Path(bpy.app.tempdir) / 'render_blender_background.py' + script_path.write_text(script_code) + + cmd = get_bl_cmd(blendfile=tmp_blend, script=script_path, background=True) + + print('Background Render...') + print(cmd) + subprocess.Popen(cmd) + def get_addon_prefs(): addon_name = __package__.split('.')[0] return bpy.context.preferences.addons[addon_name].preferences diff --git a/operators/__init__.py b/operators/__init__.py index 447cc04..cbcaf8b 100644 --- a/operators/__init__.py +++ b/operators/__init__.py @@ -1,13 +1,13 @@ # SPDX-License-Identifier: GPL-2.0-or-later -from vse_toolbox.operators import (addon, casting, imports, render, sequencer, +from vse_toolbox.operators import (addon, casting, imports, exports, sequencer, spreadsheet, tracker) modules = ( addon, casting, imports, - render, + exports, sequencer, spreadsheet, tracker diff --git a/operators/casting.py b/operators/casting.py index 99436d5..dddaa24 100644 --- a/operators/casting.py +++ b/operators/casting.py @@ -2,7 +2,7 @@ import json import bpy -from bpy.types import PropertyGroup, Operator +from bpy.types import Context, Event, PropertyGroup, Operator from bpy.props import (CollectionProperty, EnumProperty, StringProperty) from vse_toolbox.constants import CASTING_BUFFER @@ -249,15 +249,27 @@ class VSETB_OT_copy_casting(Operator): class VSETB_OT_paste_casting(Operator): bl_idname = "vse_toolbox.paste_casting" bl_label = "Paste Casting" - bl_description = "Paste Casting to active strip" + bl_description = "Paste Casting to active strip (ctrl|shift: Add, alt:Remove)" bl_options = {"REGISTER", "UNDO"} + mode : EnumProperty(items=[(m, m.title(), '') for m in ('REPLACE', 'ADD', 'REMOVE')]) + @classmethod def poll(cls, context): active_strip = context.scene.sequence_editor.active_strip if active_strip: return True + def invoke(self, context, event): + self.mode = 'REPLACE' + + if event.ctrl or event.shift: + self.mode = 'ADD' + elif event.alt: + self.mode = 'REMOVE' + + return self.execute(context) + def execute(self, context): scn = context.scene strip_settings = get_strip_settings() @@ -267,19 +279,33 @@ class VSETB_OT_paste_casting(Operator): return {"CANCELLED"} casting_datas = json.loads(CASTING_BUFFER.read_text()) + casting_ids = set(c['id'] for c in casting_datas) for strip in context.selected_sequences: strip_settings = strip.vsetb_strip_settings - strip.vsetb_strip_settings.casting.clear() + + if self.mode == 'REPLACE': + strip.vsetb_strip_settings.casting.clear() + for casting_data in casting_datas: - - item = strip.vsetb_strip_settings.casting.add() + if self.mode == 'REMOVE': + + for asset_casting in strip_settings.casting: + index = list(strip_settings.casting).index(asset_casting) + if asset_casting.id in casting_ids: + strip_settings.casting.remove(index) - item.name = casting_data['name'] - item.id = casting_data['id'] - item['_name'] = casting_data['_name'] + if strip_settings.casting_index != 0: + strip_settings.casting_index -= 1 - strip_settings.casting.update() + else: + item = strip.vsetb_strip_settings.casting.add() + + item.name = casting_data['name'] + item.id = casting_data['id'] + item['_name'] = casting_data['_name'] + + strip_settings.casting.update() return {"FINISHED"} diff --git a/operators/exports.py b/operators/exports.py new file mode 100644 index 0000000..0d4cef8 --- /dev/null +++ b/operators/exports.py @@ -0,0 +1,183 @@ + +import time +import re +from pathlib import Path +import os + +import bpy +from bpy.types import Operator +from bpy.props import BoolProperty + +from vse_toolbox.sequencer_utils import (get_strips, render_strip) +from vse_toolbox.bl_utils import (get_scene_settings, background_render) +from vse_toolbox.file_utils import install_module + + + +class VSETB_OT_render(Operator): + bl_idname = "vse_toolbox.strips_render" + bl_label = "Render Shots Strips" + bl_description = "Render Shots Strips" + bl_options = {"REGISTER", "UNDO"} + + #selected_only : BoolProperty(name="Selected Only", default=False) + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + return settings.active_project + + def invoke(self, context, event): + scn = context.scene + settings = get_scene_settings() + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + + layout = self.layout + col = layout.column() + #col.use_property_split = True + #col.use_property_decorate = False + + col.prop(settings.active_project, "render_single_file") + row = col.row() + row.enabled = settings.active_project.render_single_file + row.prop(settings.active_project, "render_template") + + col.separator() + col.prop(settings.active_project, "render_per_strip") + row = col.row() + row.enabled = settings.active_project.render_per_strip + row.prop(settings.active_project, "render_strip_template") + + #col.prop(settings, 'channel', text='Channel') + #col.prop(self, 'selected_only') + + def execute(self, context): + scn = context.scene + settings = get_scene_settings() + project = settings.active_project + + format_data = {**settings.format_data, **project.format_data} + + start_time = time.perf_counter() + if project.render_single_file: + render_path = project.render_template.format(**format_data) + background_render(output=render_path) + + if project.render_per_strip: + for strip in get_strips(channel='Shots', selected_only=True): + strip_settings = strip.vsetb_strip_settings + strip_data = {**format_data, **strip_settings.format_data} + strip_render_path = project.render_strip_template.format(**strip_data) + render_strip(strip, strip_render_path) + + self.report({"INFO"}, f'Strips rendered in {time.perf_counter()-start_time} seconds') + + return {"FINISHED"} + + +class VSETB_OT_export_edl(Operator): + bl_idname = "vse_toolbox.export_edl" + bl_label = "Export Edl" + bl_description = "Export Edl" + bl_options = {"REGISTER", "UNDO"} + + #selected_only : BoolProperty(name="Selected Only", default=False) + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + return settings.active_project + + def invoke(self, context, event): + scn = context.scene + settings = get_scene_settings() + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + + layout = self.layout + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + #col.prop(settings, 'channel', text='Channel') + #col.prop(self, 'selected_only') + + col.prop(settings.active_project, "export_edl_template") + + def execute(self, context): + opentimelineio = install_module('opentimelineio') + + from opentimelineio.schema import (Clip, Timeline, Track, ExternalReference) + from opentimelineio.opentime import (RationalTime, TimeRange) + + start_time = time.perf_counter() + + scn = context.scene + settings = get_scene_settings() + fps = scn.render.fps + + project = settings.active_project + + format_data = {**settings.format_data, **project.format_data} + output_edl = project.export_edl_template.format(**format_data) + output_edl = os.path.abspath(bpy.path.abspath(output_edl)) + output_edl = Path(output_edl) + + + timeline = Timeline(output_edl.stem) + track = Track(f"Track 1") + track.kind = "Video" + + timeline.tracks.append(track) + + for strip in get_strips(channel='Shots', selected_only=False): + strip_data = {**format_data, **strip.vsetb_strip_settings.format_data} + strip_render_path = project.render_strip_template.format(**strip_data) + + clip = Clip(Path(strip_render_path).name) + + clip.metadata.setdefault("cmx_3600", {}) + clip.metadata['cmx_3600']['reel'] = strip.name + #clip.metadata['cmx_3600']['flags'] = 'IS_CLIP' + + #clip.media_reference = ExternalReference(f'//render/{strip.name}.mov') + #clip.media_reference.name = strip.name + + #clip.available_range = TimeRange(RationalTime(0, fps), RationalTime(strip.frame_duration, fps)) + clip.source_range = TimeRange(RationalTime(strip.frame_offset_start, fps), RationalTime(strip.frame_final_duration, fps)) + + track.append(clip) + + output_edl.parent.mkdir(exist_ok=True, parents=True) + + edl = opentimelineio.adapters.write_to_string(timeline, 'cmx_3600') + output_edl.write_text(edl) + #opentimelineio.adapters.write_to_file(timeline, str(output_edl), 'cmx_3600') + + self.report({"INFO"}, f'Edl Exported in {time.perf_counter()-start_time} seconds') + + return {"FINISHED"} + + + + +classes = ( + VSETB_OT_render, + VSETB_OT_export_edl +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/render.py b/operators/render.py deleted file mode 100644 index ad20331..0000000 --- a/operators/render.py +++ /dev/null @@ -1,70 +0,0 @@ - -import time - -import bpy -from bpy.types import Operator - -from vse_toolbox.sequencer_utils import (get_strips, render_strips) -from vse_toolbox.bl_utils import get_scene_settings - - - -class VSETB_OT_render(Operator): - bl_idname = "vse_toolbox.strips_render" - bl_label = "Render Shots Strips" - bl_description = "Render Shots Strips" - bl_options = {"REGISTER", "UNDO"} - - #selected_only : BoolProperty(name="Selected Only", default=False) - - @classmethod - def poll(cls, context): - settings = get_scene_settings() - return settings.active_project - - def invoke(self, context, event): - scn = context.scene - settings = get_scene_settings() - - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - scn = context.scene - settings = get_scene_settings() - - layout = self.layout - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - #col.prop(settings, 'channel', text='Channel') - #col.prop(self, 'selected_only') - - col.prop(settings.active_project, "render_template") - - def execute(self, context): - scn = context.scene - settings = get_scene_settings() - strips = get_strips(channel='Shots', selected_only=True) - - start_time = time.perf_counter() - render_strips(strips, settings.active_project.render_template) - - self.report({"INFO"}, f'Strips rendered in {time.perf_counter()-start_time} seconds') - - return {"FINISHED"} - - - - - -classes = ( - VSETB_OT_render, -) - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/spreadsheet.py b/operators/spreadsheet.py index efff8e8..50f8d1e 100644 --- a/operators/spreadsheet.py +++ b/operators/spreadsheet.py @@ -171,6 +171,40 @@ class VSETB_OT_spreadsheet_from_clipboard(Operator): return {"FINISHED"} +class VSETB_OT_spreadsheet_to_clipboard(Operator): + bl_idname = "vse_toolbox.spreadsheet_to_clipboard" + bl_label = "Copy Spreadsheet to clipboard" + bl_description = "Copy Spreadsheet to clipboard" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + scn = context.scene + project = get_scene_settings().active_project + import_cells = project.spreadsheet_import.cells + import_cells.clear() + + SPREADSHEET.clear() + + spreadsheet = context.window_manager.clipboard + + cell_types = project.get_cell_types() + rows = list(csv.reader(StringIO(spreadsheet), delimiter='\t')) + for cell_name in rows[0]: + if not cell_name: + continue + + cell = import_cells.add() + cell.name = cell_name + cell.import_name = max(cell_types.keys(), key=lambda x: fuzzy_match(cell_name, x)) + cell.enabled = True + + project.spreadsheet_import.use_custom_cells = True + + SPREADSHEET.extend(rows) + + return {"FINISHED"} + + class VSETB_OT_import_spreadsheet(Operator): bl_idname = "vse_toolbox.import_spreadsheet" bl_label = "Import Spreadsheet" @@ -368,13 +402,7 @@ class VSETB_OT_import_spreadsheet(Operator): strip_settings.casting.update() else: - self.report({'WARNING'}, f'Asset {asset_name} not found in Project') - - - - - - + self.report({'WARNING'}, f'Asset {asset_name} not found in Project') return {"FINISHED"} @@ -446,12 +474,13 @@ class VSETB_OT_export_spreadsheet(Operator): row.prop(spreadsheet, 'show_settings', text='', icon='PREFERENCES') if spreadsheet.show_settings: col.prop(spreadsheet, "separator", expand=True, text='Separator') - if spreadsheet.format == 'CSV': + if spreadsheet.format == 'csv': col.prop(spreadsheet, "delimiter", expand=True, text='Delimiter') - col.separator() - col.prop(spreadsheet, 'open_folder', text='Open Folder') - col.prop(spreadsheet, 'export_path', text='Export Path') + if spreadsheet.format != 'Clipboard': + col.separator() + col.prop(spreadsheet, 'open_folder', text='Open Folder') + col.prop(spreadsheet, 'export_path', text='Export Path') def execute(self, context): #self.report({'ERROR'}, f'Export not implemented yet.') @@ -499,7 +528,7 @@ class VSETB_OT_export_spreadsheet(Operator): row += [strip.name] elif cell.field_name == 'DESCRIPTION': row += [strip.vsetb_strip_settings.description] - elif cell.field_name == 'FRAMES': + elif cell.field_name == 'NB_FRAMES': row += [strip.frame_final_duration] rows.append(row) @@ -531,14 +560,14 @@ class VSETB_OT_export_spreadsheet(Operator): #2023_04_11_kitsu_boris_ep01_shots export_path.parent.mkdir(parents=True, exist_ok=True) - if spreadsheet.format == 'CSV': + if spreadsheet.format == 'csv': print('Writing .csv file to', export_path) with open(str(export_path), 'w', newline='\n', encoding='utf-8') as f: writer = csv.writer(f, delimiter=spreadsheet.delimiter) for row in rows: writer.writerow(row) - elif spreadsheet.format == 'XLSX': + elif spreadsheet.format == 'xlsx': try: import openpyxl except ModuleNotFoundError: @@ -558,6 +587,15 @@ class VSETB_OT_export_spreadsheet(Operator): # Save the file workbook.save(str(export_path)) + + elif spreadsheet.format == 'Clipboard': + csv_buffer = StringIO() + + # Write CSV data to the StringIO buffer + csv_writer = csv.writer(csv_buffer, delimiter='\t') + csv_writer.writerows(rows) + + context.window_manager.clipboard = csv_buffer.getvalue() if spreadsheet.open_folder: open_file(export_path, select=True) @@ -570,6 +608,7 @@ classes = ( VSETB_MT_export_spreadsheet_presets, VSETB_OT_spreadsheet_from_file, VSETB_OT_spreadsheet_from_clipboard, + VSETB_OT_spreadsheet_to_clipboard, VSETB_OT_spreadsheet_cell_move, VSETB_OT_export_spreadsheet, VSETB_OT_import_spreadsheet diff --git a/sequencer_utils.py b/sequencer_utils.py index 18e0b8c..9ec185b 100644 --- a/sequencer_utils.py +++ b/sequencer_utils.py @@ -137,7 +137,7 @@ def get_strip_render_path(strip, template): strip_data = parse(strip.name, template=project.shot_template) - print(strip_data) + #print(strip_data) index = int(strip_data['index']) @@ -234,36 +234,31 @@ def render_strips(strips, template): scn.render.filepath = render_path ''' -def render_strips(strips, template): +def render_strip(strip, output): + output = os.path.abspath(bpy.path.abspath(output)) + scn = bpy.context.scene scene_start = scn.frame_start scene_end = scn.frame_end + scene_current = scn.frame_current render_path = scn.render.filepath - for strip in strips: - scn.frame_start = strip.frame_final_start - scn.frame_end = strip.frame_final_end - 1 - scn.render.filepath = str(get_strip_render_path(strip, template)) + scn.frame_start = strip.frame_final_start + scn.frame_end = strip.frame_final_end - 1 + scn.render.filepath = output - print(f'Render Strip to {scn.render.filepath}') - bpy.ops.render.opengl(animation=True, sequencer=True) + print(f'Render Strip to {scn.render.filepath}') + bpy.ops.render.opengl(animation=True, sequencer=True) scn.frame_start = scene_start scn.frame_end = scene_end + scn.frame_current = scene_current scn.render.filepath = render_path def import_edit(filepath, adapter="cmx_3600", channel='Shots'): opentimelineio = install_module('opentimelineio') - from opentimelineio.schema import ( - Clip, - ExternalReference, - Gap, - ImageSequenceReference, - Stack, - Timeline, - Track, - ) + from opentimelineio.schema import Clip scn = bpy.context.scene sequencer = scn.sequence_editor.sequences diff --git a/ui/panels.py b/ui/panels.py index ce5d2c0..5281491 100644 --- a/ui/panels.py +++ b/ui/panels.py @@ -208,6 +208,7 @@ class VSETB_PT_exports(VSETB_main, Panel): tracker_label = settings.tracker_name.title().replace('_', ' ') layout.operator('vse_toolbox.upload_to_tracker', text=f'Upload to {tracker_label}', icon='EXPORT') layout.operator('vse_toolbox.export_spreadsheet', text='Export Spreadsheet', icon='SPREADSHEET') + layout.operator('vse_toolbox.export_edl', text='Export edl', icon='SEQ_SEQUENCER') class VSETB_PT_casting(VSETB_main, Panel): diff --git a/ui/properties.py b/ui/properties.py index 1db43d9..db04120 100644 --- a/ui/properties.py +++ b/ui/properties.py @@ -3,6 +3,7 @@ import bpy import os from pathlib import Path +import re from bpy.props import ( BoolProperty, @@ -16,7 +17,8 @@ from bpy.types import PropertyGroup, UIList from pprint import pprint as pp from vse_toolbox.bl_utils import get_addon_prefs, get_scene_settings from vse_toolbox.constants import ASSET_PREVIEWS, TRACKERS, PREVIEWS_DIR -from vse_toolbox.file_utils import norm_str +from vse_toolbox.file_utils import norm_str, parse +from vse_toolbox.sequencer_utils import get_strip_sequence_name def get_episodes_items(self, context): @@ -187,7 +189,7 @@ def get_custom_name_items(self, context): class SpreadsheetExport(PropertyGroup): use_custom_cells: BoolProperty(default=False) - format : EnumProperty(items=[(i, i, '') for i in ('CSV', 'XLSX')]) + format : EnumProperty(items=[(i, i, '') for i in ('csv', 'xlsx', 'Clipboard')]) separator : StringProperty(default='\\n') delimiter : StringProperty(default=';') export_path : StringProperty(default='//export') @@ -245,7 +247,16 @@ class Project(PropertyGroup): name="Shot Name", default="{sequence}_sh{index:04d}") render_template : StringProperty( - name="Render Name", default="//render/{strip_name}.{ext}") + name="Movie Path", default="//render/{project_basename}.{ext}") + + render_strip_template : StringProperty( + name="Strip Path", default="//render/shots/{strip}.{ext}") + + render_per_strip: BoolProperty(name="Per Strip", default=True) + render_single_file: BoolProperty(name="Single File", default=False) + + export_edl_template : StringProperty( + name="Edl Path", default="//render/{project_basename}.edl") episode_name : EnumProperty(items=get_episodes_items, update=on_episode_updated) episodes : CollectionProperty(type=Episode) @@ -260,6 +271,23 @@ class Project(PropertyGroup): type : StringProperty() + @property + def active_episode(self): + return self.episodes.get(self.episode_name) + + @property + def format_data(self): + data = {} + + data['project'] = norm_str(self.name) + data['project_basename'] = data['project'] + + if self.active_episode: + data['episode'] = norm_str(self.episode_name) + data['project_basename'] = f"{data['project']}_{data['episode']}" + + return data + def get_cell_types(self): settings = get_scene_settings() project = settings.active_project @@ -293,7 +321,7 @@ class Project(PropertyGroup): cell = spreadsheet.cells.add() cell.name = cell_name cell.export_name = 'Name' if cell_name == 'Shot' else cell_name - cell.field_name = cell_name.upper() + cell.field_name = norm_str(cell_name, format=str.upper) cell.type = "SHOT" for metadata_type in self.metadata_types: @@ -309,7 +337,7 @@ class Project(PropertyGroup): cell = spreadsheet.cells.add() cell.name = asset_type.name cell.export_name = asset_type.name - cell.field_name = asset_type.name.upper() + cell.field_name = norm_str(asset_type.name, format=str.upper) cell.type = "ASSET_TYPE" def set_strip_metadata(self): @@ -337,6 +365,8 @@ class Project(PropertyGroup): setattr(Metadata, field_name, prop) + + class VSETB_UL_casting(UIList): order_by_type : BoolProperty(default=False) @@ -489,8 +519,24 @@ class VSETB_PGT_scene_settings(PropertyGroup): def active_episode(self): project = self.active_project if project: - return project.episodes.get(project.episode_name) + return project.active_episode + @property + def format_data(self): + data = {} + digit_matches = re.findall(r'(\d+)', bpy.data.filepath) + if len(digit_matches) == 1: + data['version'] = int(digit_matches[-1]) + + elif len(digit_matches) > 1: + data['increment'] = int(digit_matches[-1]) + data['version'] = int(digit_matches[-2]) + + suffix = Path(bpy.context.scene.render.frame_path()).suffix + data['ext'] = suffix[1:] + + return data + class VSETB_PGT_strip_settings(PropertyGroup): casting : CollectionProperty(type=AssetCasting) @@ -506,6 +552,26 @@ class VSETB_PGT_strip_settings(PropertyGroup): except IndexError: return + @property + def strip(self): + sequences = bpy.context.scene.sequence_editor.sequences_all + return next(s for s in sequences if s.vsetb_strip_settings == self) + + @property + def format_data(self): + scn = bpy.context.scene + settings = get_scene_settings() + project = settings.active_project + strip = self.strip + + data = parse(strip.name, template=project.shot_template) + data['index'] = int(data['index']) + data['sequence'] = get_strip_sequence_name(strip) + data['strip'] = strip.name + #data['shot'] = project.shot_template + + return data + classes = ( Asset, AssetCasting,