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:
2026-03-10 16:09:03 +01:00
co-authored by Claude Opus 4.6
parent c46d78f2ae
commit d2d64dc8a8
14 changed files with 548 additions and 122 deletions
+185 -9
View File
@@ -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,
)