Skip to content

Commit 8e05684

Browse files
committed
[cli] Finalize save-edl command
Fix end timecode alignment. Tested with DaVinci Resolve and added unit tests. Fixes #495
1 parent c1eb247 commit 8e05684

File tree

4 files changed

+56
-19
lines changed

4 files changed

+56
-19
lines changed

scenedetect.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@
313313
# Reel/tape name to use.
314314
#reel = AX
315315

316-
# Title to use for the EDL file. Can use $VIDEO_NAME macro.
316+
# Title to use for the EDL information. Can use $VIDEO_NAME macro.
317317
#title = $VIDEO_NAME
318318

319319

scenedetect/_cli/commands.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from xml.dom import minidom
2525
from xml.etree import ElementTree
2626

27+
import scenedetect
2728
from scenedetect._cli.config import XmlFormat
2829
from scenedetect._cli.context import CliContext
2930
from scenedetect.frame_timecode import FrameTimecode
@@ -288,19 +289,8 @@ def get_edl_timecode(timecode: FrameTimecode):
288289

289290
# Add each shot as an edit entry
290291
for i, (start, end) in enumerate(scenes):
291-
# TODO: Handle start time shift.
292292
in_tc = get_edl_timecode(start)
293-
out_tc = get_edl_timecode(end)
294-
295-
# TODO: How should the source/rec timestamps be aligned? One example I found showed:
296-
#
297-
# 001 AX V C 00:00:00:00 00:00:10:00 00:00:00:00 00:00:10:00
298-
# 002 AX V C 00:00:10:01 00:00:20:00 00:00:10:00 00:00:20:00
299-
# 003 AX V C 00:00:20:01 00:00:30:00 00:00:20:00 00:00:30:00
300-
# 004 AX V C 00:00:30:01 00:00:40:00 00:00:30:00 00:00:40:00
301-
# ^
302-
# |- Shifted by 1 frame here
303-
293+
out_tc = get_edl_timecode(end - 1) # Correct for presentation time
304294
# Format the edit entry according to CMX 3600 format
305295
event_line = f"{(i + 1):03d} {reel} V C {in_tc} {out_tc} {in_tc} {out_tc}"
306296
edl_content.append(event_line)
@@ -311,6 +301,7 @@ def get_edl_timecode(timecode: FrameTimecode):
311301
)
312302
logger.info(f"Writing scenes in EDL format to {edl_path}")
313303
with open(edl_path, "w") as f:
304+
f.write(f"* CREATED WITH PYSCENEDETECT {scenedetect.__version__}\n")
314305
f.write("\n".join(edl_content))
315306
f.write("\n")
316307

@@ -398,16 +389,15 @@ def _save_xml_fcp(
398389
output: str,
399390
):
400391
"""Saves scenes in Final Cut Pro 7 XML format."""
392+
assert scenes
401393
root = ElementTree.Element("xmeml", version="5")
402394
project = ElementTree.SubElement(root, "project")
403395
ElementTree.SubElement(project, "name").text = context.video_stream.name
404396
sequence = ElementTree.SubElement(project, "sequence")
405397
ElementTree.SubElement(sequence, "name").text = context.video_stream.name
406398

407-
# TODO: We should calculate duration from the scene list.
408-
duration = context.video_stream.duration
409-
duration = str(duration.get_seconds()) # TODO: Is float okay here?
410-
ElementTree.SubElement(sequence, "duration").text = duration
399+
duration = scenes[-1][1] - scenes[0][0]
400+
ElementTree.SubElement(sequence, "duration").text = f"{duration.get_frames()}"
411401

412402
rate = ElementTree.SubElement(sequence, "rate")
413403
ElementTree.SubElement(rate, "timebase").text = str(context.video_stream.frame_rate)
@@ -444,6 +434,8 @@ def _save_xml_fcp(
444434
ElementTree.SubElement(file_ref, "name").text = context.video_stream.name
445435
path = Path(context.video_stream.path).absolute()
446436
# TODO: Can we just use path.as_uri() here?
437+
# On Windows this should be: file://localhost/C:/Users/... according to the samples provided
438+
# from https://github.com/Breakthrough/PySceneDetect/issues/156#issuecomment-1076213412.
447439
ElementTree.SubElement(file_ref, "pathurl").text = f"file://{path}"
448440

449441
media_ref = ElementTree.SubElement(file_ref, "media")
@@ -477,6 +469,9 @@ def save_xml(
477469
# We only use scene information.
478470
del cuts
479471

472+
if not scenes:
473+
return
474+
480475
if format == XmlFormat.FCPX:
481476
_save_xml_fcpx(context, scenes, filename, output)
482477
elif format == XmlFormat.FCP:

tests/test_cli.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
# included LICENSE file, or visit one of the above pages for details.
1111
#
1212

13-
import glob
1413
import os
1514
import subprocess
1615
import typing as ty
@@ -20,6 +19,7 @@
2019
import numpy as np
2120
import pytest
2221

22+
import scenedetect
2323
from scenedetect.video_splitter import is_ffmpeg_available, is_mkvmerge_available
2424

2525
# These tests validate that the CLI itself functions correctly, mainly based on the return
@@ -739,3 +739,45 @@ def test_cli_load_scenes_round_trip():
739739
assert ground_truth.split(SPLIT_POINT)[1] == loaded_first_pass.split(SPLIT_POINT)[1]
740740
with open("testout.csv") as first, open("testout2.csv") as second:
741741
assert first.readlines() == second.readlines()
742+
743+
744+
def test_cli_save_edl(tmp_path: Path):
745+
"""Test `save-edl` command."""
746+
assert (
747+
invoke_scenedetect(
748+
"-i {VIDEO} time {TIME} {DETECTOR} save-edl",
749+
output_dir=tmp_path,
750+
)
751+
== 0
752+
)
753+
output_path = tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}.edl")
754+
assert os.path.exists(output_path)
755+
EXPECTED_EDL_OUTPUT = f"""* CREATED WITH PYSCENEDETECT {scenedetect.__version__}
756+
TITLE: {DEFAULT_VIDEO_NAME}
757+
FCM: NON-DROP FRAME
758+
759+
001 AX V C 00:00:02:00 00:00:03:17 00:00:02:00 00:00:03:17
760+
002 AX V C 00:00:03:18 00:00:05:23 00:00:03:18 00:00:05:23
761+
"""
762+
assert output_path.read_text() == EXPECTED_EDL_OUTPUT
763+
764+
765+
def test_cli_save_edl_with_params(tmp_path: Path):
766+
"""Test `save-edl` command but override the other options."""
767+
assert (
768+
invoke_scenedetect(
769+
"-i {VIDEO} time {TIME} {DETECTOR} save-edl -t title -r BX -f file_no_ext",
770+
output_dir=tmp_path,
771+
)
772+
== 0
773+
)
774+
output_path = tmp_path.joinpath("file_no_ext")
775+
assert os.path.exists(output_path)
776+
EXPECTED_EDL_OUTPUT = f"""* CREATED WITH PYSCENEDETECT {scenedetect.__version__}
777+
TITLE: title
778+
FCM: NON-DROP FRAME
779+
780+
001 BX V C 00:00:02:00 00:00:03:17 00:00:02:00 00:00:03:17
781+
002 BX V C 00:00:03:18 00:00:05:23 00:00:03:18 00:00:05:23
782+
"""
783+
assert output_path.read_text() == EXPECTED_EDL_OUTPUT

website/pages/changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,10 +627,10 @@ Development
627627
### Work In Progress
628628

629629
- [feature] New `save-xml` command supports saving scenes in Final Cut Pro format [#156](https://github.com/Breakthrough/PySceneDetect/issues/156)
630-
- [feature] New `save-edl` command supports saving scenes in EDL format CMX 3600 [#495](https://github.com/Breakthrough/PySceneDetect/issues/495)
631630

632631
### Complete
633632

633+
- [feature] New `save-edl` command supports saving scenes in EDL format CMX 3600 [#495](https://github.com/Breakthrough/PySceneDetect/issues/495)
634634
- [general] The `export-html` command is now deprecated, use `save-html` instead
635635
- [bugfix] Fix incorrect help entries for short-form arguments which suggested invalid syntax [#493](https://github.com/Breakthrough/PySceneDetect/issues/493)
636636
- [bugfix] Fix crash when using `split-video` with `-m`/`--mkvmerge` option [#473](https://github.com/Breakthrough/PySceneDetect/issues/473)

0 commit comments

Comments
 (0)