Search code examples
pythonpython-3.xxmlfinalcut

Monkey-patching OpenTimelineIO adapter to import Final Cut Pro XML


I have several video projects from Final Cut Pro that I want to use in KdenLive. I found the OpenTimelineIO project and it would solve all my problems. I installed with

$ python3 -m pip install opentimelineio
...
$ python3 -m pip show opentimelineio
Name: OpenTimelineIO
Version: 0.15.0

I tried the sample code provided on GitHub:

import opentimelineio as otio

timeline = otio.adapters.read_from_file("/path/to/file.fcpxml")
for clip in timeline.find_clips():
  print(clip.name, clip.duration())

and I get the error:

  File "~/Library/Python/3.8/lib/python/site-packages/opentimelineio_contrib/adapters/fcpx_xml.py", line 998, in _format_id_for_clip
    resource = self._compound_clip_by_id(
AttributeError: 'NoneType' object has no attribute 'find'

Following "AttributeError: 'NoneType' object has no attribute 'find'" when converting with OpenTimelineIO , I monkey-patch the source code, changing around line 991:

def _format_id_for_clip(self, clip, default_format):
    if not clip.get("ref", None) or clip.tag == "gap":
        return default_format

    resource = self._asset_by_id(clip.get("ref"))

    if resource is None:
        resource = self._compound_clip_by_id(
            clip.get("ref")
        ).find("sequence")

To:

def _format_id_for_clip(self, clip, default_format):
    if not clip.get("ref", None) or clip.tag == "gap":
        return default_format

    resource = self._asset_by_id(clip.get("ref"))

    if resource is None:
        resource = self._compound_clip_by_id(
            clip.get("ref")
        )
        if resource is None:
            return default_format
        else:
            resource = resource.find("sequence")

    return resource.get("format", default_format)

Then I get another error:

  File "/usr/local/lib/python3.11/site-packages/opentimelineio_contrib/adapters/fcpx_xml.py", line 1054, in _format_frame_duration
    total, rate = media_format.get("frameDuration").split("/")
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'split'

The offending lines are:

# --------------------
# time helpers
# --------------------
def _format_frame_duration(self, format_id):
    media_format = self._format_by_id(format_id)
    total, rate = media_format.get("frameDuration").split("/")
    rate = rate.replace("s", "")
    return total, rate

So I printed details on clips, and it seems most are 100/2500s, so I return that:

def _format_frame_duration(self, format_id):
    media_format = self._format_by_id(format_id)
    print(media_format)
    print(dir(media_format))
    try:
        print(media_format.__dict__)
        print(media_format.__dict__())
    except AttributeError:
        pass
    print([attr for attr in dir(media_format) if attr[:2] + attr[-2:] != '____' and not callable(getattr(media_format,attr))])
    print(media_format.attrib)
    print(media_format.tag)
    print(media_format.tail)
    print(media_format.text)

    if None is media_format.get("frameDuration"):
        return "100", "2500"
    
    total, rate = media_format.get("frameDuration").split("/")
    rate = rate.replace("s", "")
    return total, rate

And then that command runs, but the next throws an error:

    for clip in timeline.find_clips():
                ^^^^^^^^^^^^^^^^^^^
AttributeError: 'opentimelineio._otio.SerializableCollection' object has no attribute 'find_clips'

I try running the import from Kdenlive and get this error:

  File "/usr/local/lib/python3.11/dist-packages/opentimelineio_contrib/adapters/fcpx_xml.py", line 938, in _timing_clip
    while clip.tag not in ("clip", "asset-clip", "ref-clip"):
          ^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'tag'

Here is a Dropbox link to a complete FCP XML file that causes this error.

I submitted an issue on GitHub about the second error that I monkey-patched and it has had no activity for 3 months. These issues raise the bar quite a bit and require some knowledge of opentimelineio, so I ask or help here.

How can I further monkey-patch the OpenTimelineIO code to convert this project to Kdenlive?


Solution

  • I think the problem is with your .fcpxml file. When I debug, it throws an error on the format tag with id="r110":

    enter image description here

    Other format tags have more attributes like frameDuration, as you mentioned:

    enter image description here

    If you know the correct values for this entry, you can add it on line 1256 and change it from:

    <format id="r110" name="FFVideoFormatRateUndefined" width="1920" height="1080"/>

    To something like:

    <format id="r110" name="FFVideoFormatRateUndefined" width="1920" height="1080" frameDuration="100/2500s"/>

    When you fix this, the next error is this:

    'NoneType' object has no attribute 'find'

    When you face this error, the running variables are:

    enter image description here

    Which corresponds to the line 3219 of the file:

    <title ref="r120" offset="1848900/7500s" name="© Atletismo Emocional para crianças, 2020 - Scrolling" start="3600s" duration="57300/2500s">

    The error happens because there are no <asset> or <media> elements with id="r120".

    The r120 reference appears in two tags only, so I removed them and reran the program. This is what to remove:

    line 1345 (effect tag)
    line 3218 to 3387 (title tag)
    Lines are approximate
    (If you have any information on what id is used for this reference, put it in the <title> element, and also you have to create a <format> element as I saw the code extracts that too. If you don't have the info, then just remove the tags with r120.)

    Now it passes that line and throws another error:

    'opentimelineio._otio.SerializableCollection' object has no attribute 'find_clips'. This is awkward because the problem is with the module itself. The object does not have the method mentioned, and I think the substitution is to use each_clip(). So, to fix it, you have to change this:

    timeline.find_clips() -> timeline.each_clip()

    This will fix the problem, and it runs without any monkey-patches!