Skip to content

Commit 2b7ebbd

Browse files
committed
[save-otio] Finalize OTIO support
1 parent 4758964 commit 2b7ebbd

File tree

7 files changed

+177
-28
lines changed

7 files changed

+177
-28
lines changed

docs/cli.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,14 @@ Options
754754

755755
Output directory to save OTIO file to. Overrides global option :option:`-o/--output <scenedetect -o>`.
756756

757+
.. option:: --audio
758+
759+
Include audio track (default).
760+
761+
.. option:: --no-audio
762+
763+
Exclude audio track.
764+
757765

758766
.. _command-save-qp:
759767

@@ -795,7 +803,7 @@ Options
795803
``save-xml``
796804
========================================================================
797805

798-
Save cuts in XML format.
806+
[IN DEVELOPMENT] Save cuts in XML format.
799807

800808

801809
Options

scenedetect.cfg

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@
328328
# Name to use for the OTIO timeline. Can use $VIDEO_NAME macro.
329329
#title = $VIDEO_NAME (PySceneDetect)
330330

331+
# Include audio track (yes/no).
332+
#audio = yes
333+
331334

332335
[save-qp]
333336

@@ -341,20 +344,6 @@
341344
#disable-shift = no
342345

343346

344-
[save-xml]
345-
346-
# Filename format of XML file. Can use $VIDEO_NAME macro.
347-
#filename = $VIDEO_NAME.xml
348-
349-
# Format of the XML file. Must be one of:
350-
# - fcpx: Final Cut Pro X (default)
351-
# - fcp: Final Cut Pro 7
352-
#format = fcpx
353-
354-
# Folder to output XML file to. Overrides [global] output option.
355-
#output = /usr/tmp/images
356-
357-
358347
#
359348
# BACKEND OPTIONS
360349
#

scenedetect/_cli/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,10 +1626,10 @@ def save_qp_command(
16261626
ctx.add_command(cli_commands.save_qp, save_qp_args)
16271627

16281628

1629-
SAVE_XML_HELP = """Save cuts in XML format."""
1629+
SAVE_XML_HELP = """[IN DEVELOPMENT] Save cuts in XML format."""
16301630

16311631

1632-
@click.command("save-xml", cls=Command, help=SAVE_XML_HELP)
1632+
@click.command("save-xml", cls=Command, help=SAVE_XML_HELP, hidden=True)
16331633
@click.option(
16341634
"--filename",
16351635
"-f",
@@ -1705,20 +1705,40 @@ def save_xml_command(
17051705
help="Output directory to save OTIO file to. Overrides global option -o/--output.%s"
17061706
% (USER_CONFIG.get_help_string("save-otio", "output", show_default=False)),
17071707
)
1708+
@click.option(
1709+
"--audio",
1710+
is_flag=True,
1711+
flag_value=True,
1712+
help="Include audio track (default).",
1713+
)
1714+
@click.option(
1715+
"--no-audio",
1716+
is_flag=True,
1717+
flag_value=True,
1718+
help="Exclude audio track.",
1719+
)
17081720
@click.pass_context
17091721
def save_otio_command(
17101722
ctx: click.Context,
17111723
filename: ty.Optional[ty.AnyStr],
17121724
name: ty.Optional[ty.AnyStr],
17131725
output: ty.Optional[ty.AnyStr],
1726+
audio: bool,
1727+
no_audio: bool,
17141728
):
17151729
ctx = ctx.obj
17161730
assert isinstance(ctx, CliContext)
17171731

1732+
if audio and no_audio:
1733+
raise click.BadArgumentUsage("Only one of --audio or --no-audio can be specified.")
1734+
17181735
save_otio_args = {
17191736
"filename": ctx.config.get_value("save-otio", "filename", filename),
17201737
"name": ctx.config.get_value("save-otio", "name", name),
17211738
"output": ctx.config.get_value("save-otio", "output", output),
1739+
"audio": ctx.config.get_value(
1740+
"save-otio", "audio", True if audio else False if no_audio else None
1741+
),
17221742
}
17231743
ctx.add_command(cli_commands.save_otio, save_otio_args)
17241744

scenedetect/_cli/commands.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ def save_otio(
489489
filename: str,
490490
output: str,
491491
name: str,
492+
audio: bool,
492493
):
493494
"""Saves scenes in OTIO format."""
494495

@@ -501,7 +502,9 @@ def save_otio(
501502

502503
# List of track mapping to resource type.
503504
# TODO(#497): Allow exporting without an audio track.
504-
track_list = {"Video 1": "Video", "Audio 1": "Audio"}
505+
track_list = {"Video 1": "Video"}
506+
if audio:
507+
track_list["Audio 1"] = "Audio"
505508

506509
otio = {
507510
"OTIO_SCHEMA": "Timeline.1",

scenedetect/_cli/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ class XmlFormat(Enum):
425425
"width": 0,
426426
},
427427
"save-otio": {
428+
"audio": True,
428429
"filename": "$VIDEO_NAME.otio",
429430
"name": "$VIDEO_NAME (PySceneDetect)",
430431
"output": None,

tests/test_cli.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,3 +990,123 @@ def test_cli_save_otio(tmp_path: Path):
990990
assert output_path.read_text() == EXPECTED_OTIO_OUTPUT.replace(
991991
"{ABSOLUTE_PATH}", os.path.abspath(DEFAULT_VIDEO_PATH).replace("\\", "\\\\")
992992
)
993+
994+
995+
def test_cli_save_otio_no_audio(tmp_path: Path):
996+
"""Test `save-otio` command without audio."""
997+
assert (
998+
invoke_scenedetect(
999+
"-i {VIDEO} time {TIME} {DETECTOR} save-otio --no-audio",
1000+
output_dir=tmp_path,
1001+
)
1002+
== 0
1003+
)
1004+
output_path = tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}.otio")
1005+
assert os.path.exists(output_path)
1006+
EXPECTED_OTIO_OUTPUT = """{
1007+
"OTIO_SCHEMA": "Timeline.1",
1008+
"name": "goldeneye (PySceneDetect)",
1009+
"global_start_time": {
1010+
"OTIO_SCHEMA": "RationalTime.1",
1011+
"rate": 23.976023976023978,
1012+
"value": 0.0
1013+
},
1014+
"tracks": {
1015+
"OTIO_SCHEMA": "Stack.1",
1016+
"enabled": true,
1017+
"children": [
1018+
{
1019+
"OTIO_SCHEMA": "Track.1",
1020+
"name": "Video 1",
1021+
"enabled": true,
1022+
"children": [
1023+
{
1024+
"OTIO_SCHEMA": "Clip.2",
1025+
"name": "goldeneye.mp4",
1026+
"source_range": {
1027+
"OTIO_SCHEMA": "TimeRange.1",
1028+
"duration": {
1029+
"OTIO_SCHEMA": "RationalTime.1",
1030+
"rate": 23.976023976023978,
1031+
"value": 42.0
1032+
},
1033+
"start_time": {
1034+
"OTIO_SCHEMA": "RationalTime.1",
1035+
"rate": 23.976023976023978,
1036+
"value": 48.0
1037+
}
1038+
},
1039+
"enabled": true,
1040+
"media_references": {
1041+
"DEFAULT_MEDIA": {
1042+
"OTIO_SCHEMA": "ExternalReference.1",
1043+
"name": "goldeneye.mp4",
1044+
"available_range": {
1045+
"OTIO_SCHEMA": "TimeRange.1",
1046+
"duration": {
1047+
"OTIO_SCHEMA": "RationalTime.1",
1048+
"rate": 23.976023976023978,
1049+
"value": 1980.0
1050+
},
1051+
"start_time": {
1052+
"OTIO_SCHEMA": "RationalTime.1",
1053+
"rate": 23.976023976023978,
1054+
"value": 0.0
1055+
}
1056+
},
1057+
"available_image_bounds": null,
1058+
"target_url": "{ABSOLUTE_PATH}"
1059+
}
1060+
},
1061+
"active_media_reference_key": "DEFAULT_MEDIA"
1062+
},
1063+
{
1064+
"OTIO_SCHEMA": "Clip.2",
1065+
"name": "goldeneye.mp4",
1066+
"source_range": {
1067+
"OTIO_SCHEMA": "TimeRange.1",
1068+
"duration": {
1069+
"OTIO_SCHEMA": "RationalTime.1",
1070+
"rate": 23.976023976023978,
1071+
"value": 54.0
1072+
},
1073+
"start_time": {
1074+
"OTIO_SCHEMA": "RationalTime.1",
1075+
"rate": 23.976023976023978,
1076+
"value": 90.0
1077+
}
1078+
},
1079+
"enabled": true,
1080+
"media_references": {
1081+
"DEFAULT_MEDIA": {
1082+
"OTIO_SCHEMA": "ExternalReference.1",
1083+
"name": "goldeneye.mp4",
1084+
"available_range": {
1085+
"OTIO_SCHEMA": "TimeRange.1",
1086+
"duration": {
1087+
"OTIO_SCHEMA": "RationalTime.1",
1088+
"rate": 23.976023976023978,
1089+
"value": 1980.0
1090+
},
1091+
"start_time": {
1092+
"OTIO_SCHEMA": "RationalTime.1",
1093+
"rate": 23.976023976023978,
1094+
"value": 0.0
1095+
}
1096+
},
1097+
"available_image_bounds": null,
1098+
"target_url": "{ABSOLUTE_PATH}"
1099+
}
1100+
},
1101+
"active_media_reference_key": "DEFAULT_MEDIA"
1102+
}
1103+
],
1104+
"kind": "Video"
1105+
}
1106+
]
1107+
}
1108+
}
1109+
"""
1110+
assert output_path.read_text() == EXPECTED_OTIO_OUTPUT.replace(
1111+
"{ABSOLUTE_PATH}", os.path.abspath(DEFAULT_VIDEO_PATH).replace("\\", "\\\\")
1112+
)

website/pages/changelog.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ Releases
44

55
## PySceneDetect 0.6
66

7+
### PySceneDetect 0.6.6 (March 9, 2025)
8+
9+
#### Release Notes
10+
11+
PySceneDetect v0.6.6 introduces new output formats, which improve compatibility with popular video editors (e.g. DaVinci Resolve). Also included are several important bugfixes.
12+
13+
#### Changelog
14+
15+
- [feature] New `save-otio` command supports saving scenes in OTIO format [#497](https://github.com/Breakthrough/PySceneDetect/issues/497)
16+
- [feature] New `save-edl` command supports saving scenes in EDL format CMX 3600 [#495](https://github.com/Breakthrough/PySceneDetect/issues/495)
17+
- [general] The `export-html` command is now deprecated, use `save-html` instead
18+
- [bugfix] Fix incorrect help entries for short-form arguments which suggested invalid syntax [#493](https://github.com/Breakthrough/PySceneDetect/issues/493)
19+
- [bugfix] Fix crash when using `split-video` with `-m`/`--mkvmerge` option [#473](https://github.com/Breakthrough/PySceneDetect/issues/473)
20+
- [bugfix] Fix incorrect default filename template for `split-video` command with `-m`/`--mkvmerge` option
21+
- [bugfix] Fix inconsistent filenames when using `split_video_mkvmerge()` function in `scenedetect.video_splitter` module
22+
23+
724
### PySceneDetect 0.6.5 (November 24, 2024)
825

926
#### Release Notes
@@ -622,17 +639,8 @@ Both the Windows installer and portable distributions now include signed executa
622639
Development
623640
==========================================================
624641

625-
## PySceneDetect 0.6.6 (In Development)
642+
## PySceneDetect 0.7 (In Development)
626643

627644
### Work In Progress
628645

629646
- [feature] New `save-xml` command supports saving scenes in Final Cut Pro format [#156](https://github.com/Breakthrough/PySceneDetect/issues/156)
630-
631-
### Complete
632-
633-
- [feature] New `save-edl` command supports saving scenes in EDL format CMX 3600 [#495](https://github.com/Breakthrough/PySceneDetect/issues/495)
634-
- [general] The `export-html` command is now deprecated, use `save-html` instead
635-
- [bugfix] Fix incorrect help entries for short-form arguments which suggested invalid syntax [#493](https://github.com/Breakthrough/PySceneDetect/issues/493)
636-
- [bugfix] Fix crash when using `split-video` with `-m`/`--mkvmerge` option [#473](https://github.com/Breakthrough/PySceneDetect/issues/473)
637-
- [bugfix] Fix incorrect default filename template for `split-video` command with `-m`/`--mkvmerge` option
638-
- [bugfix] Fix inconsistent filenames when using `split_video_mkvmerge()` function in `scenedetect.video_splitter` module

0 commit comments

Comments
 (0)