Blender 5.0 compat + STB XML import + sequence/stamps improvements
- Fix Blender 5.0 API: active_sequence_strip → active_strip, bracket property access on AddonPreferences, _RestrictContext during register() - Fix new_effect() frame_end → length for Blender 5.0 - Fix OTIO adapter kwargs (rate/ignore_timecode_mismatch only for cmx_3600) - Fix OTIO global_start_time RationalTime → int conversion - Add Import STB XML operator with movie strip import and XML patching for Storyboard Pro transitions missing <alignment> - Add Create Sequence Strip operator (select shots → create sequence) - Improve Set Stamps: channel at top, no conflict with existing strips, use raw strip names in templates - TVSHOW episode fixes: sequence loading, episode creation - Kitsu: new_asset, new_episode, admin_connect fix, gazu version pin - Fix escape warnings in file_utils regex patterns - Guard handler registration against duplicates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+185
-9
@@ -15,7 +15,7 @@ from vse_toolbox.constants import (EDITS, EDIT_SUFFIXES, MOVIES, MOVIE_SUFFIXES,
|
||||
from vse_toolbox.sequencer_utils import (clean_sequencer, import_edit, import_movie,
|
||||
import_sound, get_strips, get_channel_index, get_empty_channel, scale_clip_to_fit)
|
||||
|
||||
from vse_toolbox.bl_utils import (get_scene_settings, get_addon_prefs, get_scene_settings, abspath)
|
||||
from vse_toolbox.bl_utils import (get_scene_settings, get_addon_prefs, abspath)
|
||||
from vse_toolbox.file_utils import install_module, parse, find_last, expand
|
||||
|
||||
|
||||
@@ -90,6 +90,15 @@ class VSETB_OT_import_files(Operator):
|
||||
|
||||
import_edit : BoolProperty(name='', default=True)
|
||||
edit: EnumProperty(name='', items=lambda s, c: EDITS)
|
||||
edit_adapter: EnumProperty(
|
||||
name='Format',
|
||||
items=[
|
||||
('AUTO', "Auto-detect", "Detect format from file extension"),
|
||||
('cmx_3600', "EDL (CMX 3600)", "Standard Edit Decision List"),
|
||||
('fcp_xml', "FCP 7 XML", "Final Cut Pro 7 XML (Toon Boom Storyboard Pro)"),
|
||||
],
|
||||
default='AUTO'
|
||||
)
|
||||
match_by : EnumProperty(name='Match By', items=[('NAME', 'Name', ''), ('INDEX', 'Index', '')])
|
||||
|
||||
import_movie : BoolProperty(name='', default=False)
|
||||
@@ -119,6 +128,8 @@ class VSETB_OT_import_files(Operator):
|
||||
sub.active = self.import_edit
|
||||
sub.prop(self, 'edit')
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, 'edit_adapter', text='Format')
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, 'match_by', expand=True)
|
||||
|
||||
layout.separator()
|
||||
@@ -144,7 +155,7 @@ class VSETB_OT_import_files(Operator):
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
sequencer = context.scene.sequence_editor.sequences
|
||||
sequencer = context.scene.sequence_editor.strips
|
||||
|
||||
edit_filepath = Path(self.directory, self.edit)
|
||||
if not edit_filepath.exists():
|
||||
@@ -168,7 +179,12 @@ class VSETB_OT_import_files(Operator):
|
||||
if self.import_edit:
|
||||
print(f'[>.] Loading Edit from: {str(edit_filepath)}')
|
||||
|
||||
import_edit(edit_filepath, adapter="cmx_3600", match_by=self.match_by)
|
||||
adapter = self.edit_adapter
|
||||
if adapter == 'AUTO':
|
||||
ext = edit_filepath.suffix.lower()
|
||||
adapter = 'fcp_xml' if ext == '.xml' else 'cmx_3600'
|
||||
|
||||
import_edit(edit_filepath, adapter=adapter, match_by=self.match_by)
|
||||
|
||||
if self.import_movie:
|
||||
print(f'[>.] Loading Movie from: {str(movie_filepath)}')
|
||||
@@ -190,7 +206,7 @@ class VSETB_OT_import_files(Operator):
|
||||
print(f'[>.] Loading Audio from: {str(movie_filepath)}')
|
||||
import_sound(movie_filepath)
|
||||
|
||||
context.scene.sequence_editor.sequences.update()
|
||||
context.scene.sequence_editor.strips.update()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -442,8 +458,9 @@ class VSETB_OT_import_shots(Operator):
|
||||
|
||||
for asset_data in casting_data:
|
||||
item = strip_settings.casting.add()
|
||||
item.name = asset_data['asset_name']
|
||||
item.name = asset_data['asset_name']
|
||||
item.id = asset_data['asset_id']
|
||||
item.instance = asset_data.get('nb_occurences', 1)
|
||||
item['_name'] = asset_data['asset_name']
|
||||
|
||||
strip_settings.casting.update()
|
||||
@@ -461,6 +478,10 @@ class VSETB_OT_import_shots(Operator):
|
||||
task_types = [t for t in project.task_types if t.import_enabled]
|
||||
sequences = [s for s in project.sequences if s.import_enabled]
|
||||
|
||||
if not sequences:
|
||||
self.report({'ERROR'}, "No sequences selected. For episodic projects, select an episode first.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
conformed = False
|
||||
|
||||
if import_shots.clear:
|
||||
@@ -484,12 +505,12 @@ class VSETB_OT_import_shots(Operator):
|
||||
frames = int(frames)
|
||||
frame_end = frame_index + frames
|
||||
|
||||
strip = scn.sequence_editor.sequences.new_effect(
|
||||
strip = scn.sequence_editor.strips.new_effect(
|
||||
name=shot_data['name'],
|
||||
type='COLOR',
|
||||
channel=get_channel_index('Shots'),
|
||||
frame_start=frame_index,
|
||||
frame_end=frame_index + frames
|
||||
length=frames
|
||||
)
|
||||
strip.blend_alpha = 0
|
||||
strip.color = (0.5, 0.5, 0.5)
|
||||
@@ -534,12 +555,12 @@ class VSETB_OT_import_shots(Operator):
|
||||
|
||||
frame_index += frames
|
||||
|
||||
strip = scn.sequence_editor.sequences.new_effect(
|
||||
strip = scn.sequence_editor.strips.new_effect(
|
||||
name=sequence.name,
|
||||
type='COLOR',
|
||||
channel=get_channel_index('Sequences'),
|
||||
frame_start=sequence_start,
|
||||
frame_end=frame_index
|
||||
length=frame_index - sequence_start
|
||||
)
|
||||
strip.blend_alpha = 0
|
||||
strip.color = (0.25, 0.25, 0.25)
|
||||
@@ -673,6 +694,160 @@ class VSETB_OT_import_shots(Operator):
|
||||
return True
|
||||
|
||||
|
||||
class VSETB_OT_import_stb_xml(Operator):
|
||||
bl_idname = "vse_toolbox.import_stb_xml"
|
||||
bl_label = "Import STB XML"
|
||||
bl_description = "Import Toon Boom Storyboard Pro FCP XML export with movie strips"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="Path to the Storyboard Pro FCP XML export",
|
||||
subtype='FILE_PATH',
|
||||
)
|
||||
filter_glob: StringProperty(default="*.xml", options={'HIDDEN'})
|
||||
|
||||
import_movies: BoolProperty(
|
||||
name="Import Movies",
|
||||
description="Import matching .mov files from the same directory",
|
||||
default=True,
|
||||
)
|
||||
clean_sequencer: BoolProperty(
|
||||
name="Clean Sequencer",
|
||||
description="Remove all existing strips before import",
|
||||
default=True,
|
||||
)
|
||||
conform_resolution: BoolProperty(
|
||||
name="Conform Resolution",
|
||||
description="Set scene resolution and FPS from the XML metadata",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return get_scene_settings().active_project
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def _parse_xml_metadata(self, filepath):
|
||||
"""Extract resolution and FPS from the FCP XML."""
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse(filepath)
|
||||
root = tree.getroot()
|
||||
|
||||
meta = {}
|
||||
fmt = root.find('.//sequence/media/video/format/samplecharacteristics')
|
||||
if fmt is not None:
|
||||
w = fmt.find('width')
|
||||
h = fmt.find('height')
|
||||
if w is not None and h is not None:
|
||||
meta['width'] = int(w.text)
|
||||
meta['height'] = int(h.text)
|
||||
rate = fmt.find('rate/timebase')
|
||||
if rate is not None:
|
||||
meta['fps'] = int(rate.text)
|
||||
|
||||
return meta
|
||||
|
||||
def execute(self, context):
|
||||
scn = context.scene
|
||||
xml_path = Path(self.filepath)
|
||||
|
||||
if not xml_path.exists():
|
||||
self.report({'ERROR'}, f"File not found: {xml_path}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
xml_dir = xml_path.parent
|
||||
|
||||
# Conform scene resolution/fps from XML metadata
|
||||
if self.conform_resolution:
|
||||
meta = self._parse_xml_metadata(str(xml_path))
|
||||
if 'width' in meta:
|
||||
scn.render.resolution_x = meta['width']
|
||||
scn.render.resolution_y = meta['height']
|
||||
print(f'[STB] Set resolution to {meta["width"]}x{meta["height"]}')
|
||||
if 'fps' in meta:
|
||||
scn.render.fps = meta['fps']
|
||||
print(f'[STB] Set FPS to {meta["fps"]}')
|
||||
|
||||
# Clean sequencer if requested
|
||||
if self.clean_sequencer:
|
||||
scn.sequence_editor_clear()
|
||||
scn.sequence_editor_create()
|
||||
|
||||
if not scn.sequence_editor:
|
||||
scn.sequence_editor_create()
|
||||
|
||||
# Set up channels
|
||||
channels = scn.sequence_editor.channels
|
||||
channel_names = {'Shots': 1, 'STB Video': 2}
|
||||
for name, idx in channel_names.items():
|
||||
if idx < len(channels):
|
||||
channels[idx].name = name
|
||||
|
||||
# Patch XML for OTIO compatibility (STB Pro exports transitions without <alignment>)
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse(str(xml_path))
|
||||
patched = False
|
||||
for trans in tree.iter('transitionitem'):
|
||||
if trans.find('alignment') is None:
|
||||
align = ET.SubElement(trans, 'alignment')
|
||||
align.text = 'center'
|
||||
patched = True
|
||||
|
||||
if patched:
|
||||
patched_path = Path(bpy.app.tempdir) / xml_path.name
|
||||
tree.write(str(patched_path), xml_declaration=True, encoding='UTF-8')
|
||||
import_xml = str(patched_path)
|
||||
print(f'[STB] Patched {xml_path.name} (added missing <alignment> to transitions)')
|
||||
else:
|
||||
import_xml = str(xml_path)
|
||||
|
||||
# Import edit (COLOR strips on Shots channel)
|
||||
print(f'[STB] Importing edit from: {xml_path}')
|
||||
import_edit(import_xml, adapter='fcp_xml', channel='Shots')
|
||||
|
||||
# Import matching movie files
|
||||
if self.import_movies:
|
||||
shot_strips = get_strips(channel='Shots')
|
||||
stb_channel = get_channel_index('STB Video')
|
||||
|
||||
for strip in shot_strips:
|
||||
# Try to find a matching .mov file by strip name
|
||||
mov_path = xml_dir / f"{strip.name}.mov"
|
||||
if not mov_path.exists():
|
||||
# Try source_name (set by import_edit)
|
||||
source = strip.vsetb_strip_settings.source_name
|
||||
if source:
|
||||
mov_path = xml_dir / f"{Path(source).stem}.mov"
|
||||
|
||||
if mov_path.exists():
|
||||
movie_strip = scn.sequence_editor.strips.new_movie(
|
||||
name=strip.name,
|
||||
filepath=str(mov_path),
|
||||
channel=stb_channel,
|
||||
frame_start=strip.frame_final_start,
|
||||
)
|
||||
movie_strip.frame_final_end = strip.frame_final_end
|
||||
|
||||
# Scale to fit scene resolution
|
||||
scale_clip_to_fit(movie_strip)
|
||||
print(f'[STB] Imported movie: {mov_path.name}')
|
||||
else:
|
||||
print(f'[STB] No movie found for strip: {strip.name}')
|
||||
|
||||
scn.frame_start = 0
|
||||
scn.frame_end = max(
|
||||
(s.frame_final_end for s in scn.sequence_editor.strips),
|
||||
default=scn.frame_end
|
||||
)
|
||||
|
||||
self.report({'INFO'}, f"Imported {len(get_strips(channel='Shots'))} shots from STB XML")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
classes = (
|
||||
VSETB_OT_select_sequence,
|
||||
VSETB_OT_unselect_sequence,
|
||||
@@ -683,6 +858,7 @@ classes = (
|
||||
VSETB_UL_import_task,
|
||||
VSETB_OT_auto_select_files,
|
||||
VSETB_OT_import_files,
|
||||
VSETB_OT_import_stb_xml,
|
||||
VSETB_OT_import_shots,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user