From 20ab08626b50bf26354a540624d3529f52bc5c02 Mon Sep 17 00:00:00 2001 From: Javanaut Date: Fri, 22 May 2026 20:11:05 +0200 Subject: [PATCH] TF Fix styled ASS font tracks --- SCRATCHPAD.md | 4 ++ src/ffx/cli.py | 24 +++++-- src/ffx/media_descriptor.py | 43 ++++++++++++ .../subtrack_mapping/test_cli_bundle.py | 67 +++++++++++++++++++ tests/support/ffx_bundle.py | 4 ++ 5 files changed, 137 insertions(+), 5 deletions(-) diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md index 89eaddd..2d68a22 100644 --- a/SCRATCHPAD.md +++ b/SCRATCHPAD.md @@ -69,3 +69,7 @@ ## Delete When - Delete this scratchpad once the optimization backlog is either converted into issues/work items or distilled into durable project guidance. + + + +## TODO: Review styled ASS separate handling \ No newline at end of file diff --git a/src/ffx/cli.py b/src/ffx/cli.py index 58e4878..bd8dcb1 100755 --- a/src/ffx/cli.py +++ b/src/ffx/cli.py @@ -1393,13 +1393,22 @@ def convert(ctx, from ffx.attachment_format import AttachmentFormat - if ([smd for smd in sourceMediaDescriptor.getSubtitleTracks() - if smd.getCodec() == TrackCodec.ASS] - and [amd for amd in sourceMediaDescriptor.getAttachmentTracks() - if amd.getAttachmentFormat() == AttachmentFormat.TTF]): - + styledAssSourceDetected = ( + sourceMediaDescriptor.hasStyledAssSubtitlesWithFontAttachments() + ) + if styledAssSourceDetected: + styledAssMessage = ( + "Styled ASS subtitles with embedded font attachments detected; " + + "preserving source font attachments." + ) + click.echo(styledAssMessage) targetFormat = '' targetExtension = 'mkv' + if context['import_subtitles']: + raise click.ClickException( + "External subtitle import is incompatible with styled ASS " + + "sources that carry embedded font attachments." + ) #HINT: This is None if the filename did not match anything in database @@ -1426,6 +1435,11 @@ def convert(ctx, else: targetMediaDescriptor = currentPattern.getMediaDescriptor(ctx.obj) + if styledAssSourceDetected: + targetMediaDescriptor = targetMediaDescriptor.withoutAttachmentTracks( + AttachmentFormat.TTF, + context=ctx.obj, + ) checkUniqueDispositions(context, targetMediaDescriptor) currentShowDescriptor = currentPattern.getShowDescriptor(ctx.obj) diff --git a/src/ffx/media_descriptor.py b/src/ffx/media_descriptor.py index ad9cf62..5281398 100644 --- a/src/ffx/media_descriptor.py +++ b/src/ffx/media_descriptor.py @@ -329,6 +329,49 @@ class MediaDescriptor: if s.getType() == TrackType.ATTACHMENT ] + def hasStyledAssSubtitlesWithFontAttachments(self) -> bool: + return ( + any( + trackDescriptor.getCodec() == TrackCodec.ASS + for trackDescriptor in self.getSubtitleTracks() + ) + and any( + trackDescriptor.getAttachmentFormat() == AttachmentFormat.TTF + for trackDescriptor in self.getAttachmentTracks() + ) + ) + + def withoutAttachmentTracks( + self, + attachmentFormat: AttachmentFormat | None = None, + context: dict | None = None, + ): + filteredTrackDescriptors = [] + for trackDescriptor in self.__trackDescriptors: + if trackDescriptor.getType() == TrackType.ATTACHMENT and ( + attachmentFormat is None + or trackDescriptor.getAttachmentFormat() == attachmentFormat + ): + continue + filteredTrackDescriptors.append( + trackDescriptor.clone( + context=context if context is not None else self.__context + ) + ) + + kwargs = { + MediaDescriptor.TAGS_KEY: dict(self.__mediaTags), + MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY: filteredTrackDescriptors, + } + if context is not None: + kwargs[MediaDescriptor.CONTEXT_KEY] = context + elif self.__context: + kwargs[MediaDescriptor.CONTEXT_KEY] = self.__context + + filteredMediaDescriptor = MediaDescriptor(**kwargs) + filteredMediaDescriptor.reindexSubIndices() + return filteredMediaDescriptor + def getImportFileTokens(self, use_sub_index: bool = True): """Generate ffmpeg import options for external stream files""" diff --git a/tests/integration/subtrack_mapping/test_cli_bundle.py b/tests/integration/subtrack_mapping/test_cli_bundle.py index 11e0a30..b472c2c 100644 --- a/tests/integration/subtrack_mapping/test_cli_bundle.py +++ b/tests/integration/subtrack_mapping/test_cli_bundle.py @@ -18,6 +18,7 @@ from tests.support.ffx_bundle import ( write_vtt, ) +from ffx.attachment_format import AttachmentFormat from ffx.track_type import TrackType try: @@ -280,6 +281,72 @@ class SubtrackMappingBundleTests(unittest.TestCase): self.assertIn("non-existent source track #99", error_output) self.assertFalse(expected_output_path(self.workdir, source_filename).exists()) + def test_styled_ass_source_preserves_current_font_attachments_when_pattern_count_differs(self): + source_filename = "styled_ass_s01e01.mkv" + source_path = create_source_fixture( + self.workdir, + source_filename, + [ + SourceTrackSpec(TrackType.VIDEO, identity="video-0"), + SourceTrackSpec(TrackType.AUDIO, identity="audio-1", language="eng"), + SourceTrackSpec( + TrackType.SUBTITLE, + identity="subtitle-2", + language="eng", + subtitle_lines=("styled subtitle payload",), + ), + SourceTrackSpec(TrackType.ATTACHMENT, attachment_name="current.ttf"), + ], + subtitle_encoder="ass", + ) + + prepare_pattern_database( + self.database_path, + r"^styled_ass_(s[0-9]+e[0-9]+)\.mkv$", + [ + PatternTrackSpec(index=0, source_index=0, track_type=TrackType.VIDEO), + PatternTrackSpec(index=1, source_index=1, track_type=TrackType.AUDIO), + PatternTrackSpec(index=2, source_index=2, track_type=TrackType.SUBTITLE), + PatternTrackSpec( + index=3, + source_index=3, + track_type=TrackType.ATTACHMENT, + attachment_format=AttachmentFormat.TTF, + ), + PatternTrackSpec( + index=4, + source_index=4, + track_type=TrackType.ATTACHMENT, + attachment_format=AttachmentFormat.TTF, + ), + ], + ) + + completed = run_ffx_convert( + self.workdir, + self.home_dir, + self.database_path, + "--video-encoder", + "copy", + "--no-tmdb", + "--no-prompt", + "--no-signature", + str(source_path), + ) + self.assertCompleted(completed) + self.assertIn("Styled ASS subtitles", completed.stdout) + + output_path = expected_output_path(self.workdir, source_filename) + streams = ffprobe_json(output_path)["streams"] + + self.assertEqual( + [stream["codec_type"] for stream in streams], + ["video", "audio", "subtitle", "attachment"], + ) + self.assertEqual(streams[2]["codec_name"], "ass") + self.assertEqual(streams[3]["codec_name"], "ttf") + self.assertEqual(get_tag(streams[3], "filename"), "current.ttf") + def test_external_subtitle_file_replaces_payload_and_overrides_metadata(self): source_filename = "substitute_s01e01.mkv" self.write_config( diff --git a/tests/support/ffx_bundle.py b/tests/support/ffx_bundle.py index 13d0ff7..800b62e 100644 --- a/tests/support/ffx_bundle.py +++ b/tests/support/ffx_bundle.py @@ -18,6 +18,7 @@ if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) +from ffx.attachment_format import AttachmentFormat from ffx.audio_layout import AudioLayout from ffx.database import databaseContext from ffx.pattern_controller import PatternController @@ -56,6 +57,7 @@ class PatternTrackSpec: tags: Mapping[str, str] = field(default_factory=dict) dispositions: tuple[TrackDisposition, ...] = () audio_layout: AudioLayout = AudioLayout.LAYOUT_STEREO + attachment_format: AttachmentFormat = AttachmentFormat.UNKNOWN def make_logger(name: str) -> logging.Logger: @@ -299,6 +301,8 @@ def prepare_pattern_database(database_path: Path, filename_pattern: str, track_s } if track.track_type == TrackType.AUDIO: kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = track.audio_layout + if track.track_type == TrackType.ATTACHMENT: + kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY] = track.attachment_format track_descriptors.append(TrackDescriptor(**kwargs)) pattern_id = PatternController(context).savePatternSchema(