From 69b3c9c5966086fdc0f18525d3622d4467b1406c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 16 Sep 2024 04:12:56 -0400 Subject: [PATCH] Decouple timeline logic in fcp11 code --- auto_editor/edit.py | 8 +++++- auto_editor/formats/fcp11.py | 44 +++++++++++++-------------------- auto_editor/formats/utils.py | 18 -------------- auto_editor/timeline.py | 47 +++++++++++++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 47 deletions(-) diff --git a/auto_editor/edit.py b/auto_editor/edit.py index a70cb65bc..cea5fe016 100644 --- a/auto_editor/edit.py +++ b/auto_editor/edit.py @@ -236,7 +236,13 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None: from auto_editor.formats.fcp11 import fcp11_write_xml is_resolve = export.startswith("resolve") - fcp11_write_xml(export_ops["name"], ffmpeg, output, is_resolve, tl, log) + + if is_resolve: + from auto_editor.timeline import set_stream_to_0 + + set_stream_to_0(tl, ffmpeg, log) + + fcp11_write_xml(export_ops["name"], output, is_resolve, tl, log) return if export == "shotcut": diff --git a/auto_editor/formats/fcp11.py b/auto_editor/formats/fcp11.py index 88e2874f8..1f637baea 100644 --- a/auto_editor/formats/fcp11.py +++ b/auto_editor/formats/fcp11.py @@ -3,17 +3,15 @@ from typing import TYPE_CHECKING, Any, cast from xml.etree.ElementTree import Element, ElementTree, SubElement, indent -from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo - -from .utils import make_tracks_dir - if TYPE_CHECKING: from collections.abc import Sequence from fractions import Fraction + from auto_editor.ffwrapper import FileInfo from auto_editor.timeline import TlAudio, TlVideo, v3 from auto_editor.utils.log import Log + """ Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later. @@ -54,7 +52,7 @@ def make_name(src: FileInfo, tb: Fraction) -> str: def fcp11_write_xml( - group_name: str, ffmpeg: FFmpeg, output: str, resolve: bool, tl: v3, log: Log + group_name: str, output: str, resolve: bool, tl: v3, log: Log ) -> None: def fraction(val: int) -> str: if val == 0: @@ -68,23 +66,10 @@ def fraction(val: int) -> str: src_dur = int(src.duration * tl.tb) tl_dur = src_dur if resolve else tl.out_len() - all_srcs: list[FileInfo] = [src] - all_refs: list[str] = ["r2"] - if resolve and len(src.audios) > 1: - fold = make_tracks_dir(src) - - for i in range(1, len(src.audios)): - newtrack = fold / f"{i}.wav" - ffmpeg.run( - ["-i", f"{src.path.resolve()}", "-map", f"0:a:{i}", f"{newtrack}"] - ) - all_srcs.append(initFileInfo(f"{newtrack}", log)) - all_refs.append(f"r{(i + 1) * 2}") - fcpxml = Element("fcpxml", version="1.10" if resolve else "1.11") resources = SubElement(fcpxml, "resources") - for i, one_src in enumerate(all_srcs): + for i, one_src in enumerate(tl.unique_sources()): SubElement( resources, "format", @@ -126,13 +111,6 @@ def fraction(val: int) -> str: ) spine = SubElement(sequence, "spine") - if tl.v and tl.v[0]: - clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0]) - elif tl.a and tl.a[0]: - clips = tl.a[0] - else: - clips = [] - def make_clip(ref: str, clip: TlVideo | TlAudio) -> None: clip_properties = { "name": proj_name, @@ -157,7 +135,19 @@ def make_clip(ref: str, clip: TlVideo | TlAudio) -> None: interp="smooth2", ) - for my_ref in all_refs: + if tl.v and tl.v[0]: + clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0]) + elif tl.a and tl.a[0]: + clips = tl.a[0] + else: + clips = [] + + all_refs: list[str] = ["r2"] + if resolve: + for i in range(1, len(tl.a)): + all_refs.append(f"r{(i + 1) * 2}") + + for my_ref in reversed(all_refs): for clip in clips: make_clip(my_ref, clip) diff --git a/auto_editor/formats/utils.py b/auto_editor/formats/utils.py index 61c03aefe..551809039 100644 --- a/auto_editor/formats/utils.py +++ b/auto_editor/formats/utils.py @@ -4,9 +4,6 @@ from xml.etree.ElementTree import Element if TYPE_CHECKING: - from pathlib import Path - - from auto_editor.ffwrapper import FileInfo from auto_editor.utils.log import Log @@ -19,21 +16,6 @@ def show(ele: Element, limit: int, depth: int = 0) -> None: show(child, limit, depth + 1) -def make_tracks_dir(src: FileInfo) -> Path: - from os import mkdir - from shutil import rmtree - - fold = src.path.parent / f"{src.path.stem}_tracks" - - try: - mkdir(fold) - except OSError: - rmtree(fold) - mkdir(fold) - - return fold - - class Validator: def __init__(self, log: Log): self.log = log diff --git a/auto_editor/timeline.py b/auto_editor/timeline.py index d7d65389e..195ef50aa 100644 --- a/auto_editor/timeline.py +++ b/auto_editor/timeline.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +from auto_editor.ffwrapper import initFileInfo from auto_editor.lib.contracts import * from auto_editor.utils.cmdkw import Required, pAttr, pAttrs from auto_editor.utils.types import color, natural, number, threshold @@ -10,10 +11,12 @@ if TYPE_CHECKING: from collections.abc import Iterator from fractions import Fraction + from pathlib import Path from typing import Any - from auto_editor.ffwrapper import FileInfo + from auto_editor.ffwrapper import FFmpeg, FileInfo from auto_editor.utils.chunks import Chunks + from auto_editor.utils.log import Log @dataclass(slots=True) @@ -241,6 +244,13 @@ def sources(self) -> Iterator[FileInfo]: for a in aclips: yield a.src + def unique_sources(self) -> Iterator[FileInfo]: + seen = set() + for source in self.sources: + if source.path not in seen: + seen.add(source.path) + yield source + def _duration(self, layer: Any) -> int: total_dur = 0 for clips in layer: @@ -276,3 +286,38 @@ def as_dict(self) -> dict: "v": v, "a": a, } + + +def make_tracks_dir(path: Path) -> Path: + from os import mkdir + from shutil import rmtree + + tracks_dir = path.parent / f"{path.stem}_tracks" + + try: + mkdir(tracks_dir) + except OSError: + rmtree(tracks_dir) + mkdir(tracks_dir) + + return tracks_dir + + +def set_stream_to_0(tl: v3, ffmpeg: FFmpeg, log: Log) -> None: + src = tl.src + assert src is not None + fold = make_tracks_dir(src.path) + cache: dict[Path, FileInfo] = {} + + def make_track(i: int, path: Path) -> FileInfo: + newtrack = fold / f"{path.stem}_{i}.wav" + if newtrack not in cache: + ffmpeg.run(["-i", f"{path}", "-map", f"0:a:{i}", f"{newtrack}"]) + cache[newtrack] = initFileInfo(f"{newtrack}", log) + return cache[newtrack] + + for alayer in tl.a: + for aobj in alayer: + if aobj.stream > 0: + aobj.src = make_track(aobj.stream, aobj.src.path) + aobj.stream = 0