diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md index 368f60f..f9931d0 100644 --- a/SCRATCHPAD.md +++ b/SCRATCHPAD.md @@ -74,6 +74,7 @@ - No explicit prioritization owner or milestone for the optimization backlog. - No benchmark or timing harness exists for startup, probe, DB, or conversion orchestration overhead. - Repo hygiene is still mixed with generated artifacts and some clearly unfinished files. +- The legacy TMDB-backed `Scenario 4` path is currently blocked by a pattern/track regression: `Patterns must define at least one track before they can be stored.` This surfaced while rerunning TMDB-dependent checks after the zero-track pattern hardening. ## Next @@ -81,6 +82,7 @@ 2. Tackle the cheapest remaining product-surface cleanup first: - placeholder UI surfaces and dead helper cleanup. 3. Continue replacing oversized legacy test matrices with focused modern integration and unit coverage. +4. Triage the legacy `Scenario 4` pattern/track failure and decide whether to fix the harness, adapt it to the zero-track guard, or retire that path during the ongoing test-suite migration. ## Delete When diff --git a/requirements/tests.md b/requirements/tests.md index 61c269c..9dbfd92 100644 --- a/requirements/tests.md +++ b/requirements/tests.md @@ -14,6 +14,13 @@ that area. - Agents shall not silently substitute `python`, `python3`, or another interpreter for Python-side test work. - If `~/.local/share/ffx.venv/bin/python` is missing or not executable, agents shall stop and report the missing venv instead of continuing with Python-side test execution. +## Shell Environment Requirement + +- Agents shall source `~/.bashrc` from an interactive Bash shell before running TMDB-dependent test commands or TMDB-dependent `python -m ffx ...` test invocations. +- Agents shall not source `~/.bashrc.d/interactive/77_tmdb.sh` directly for normal test work; `~/.bashrc` is the required entry point. +- In automation this means agents shall use an interactive Bash invocation such as `bash -ic 'source ~/.bashrc && ...'`, because a non-interactive `bash -lc` returns from `~/.bashrc` before the interactive fragments are loaded. +- If sourcing `~/.bashrc` still does not provide required shell environment such as `TMDB_API_KEY`, agents shall stop and report the missing environment instead of continuing with TMDB-dependent test execution. + ## Current Harness - Entrypoint: `~/.local/share/ffx.venv/bin/python tests/legacy_runner.py run` diff --git a/src/ffx/ffx_controller.py b/src/ffx/ffx_controller.py index a4907ff..52ec099 100644 --- a/src/ffx/ffx_controller.py +++ b/src/ffx/ffx_controller.py @@ -171,6 +171,18 @@ class FfxController(): return [outputFilePath] + def generateEncodingMetadataTags(self, videoEncoder: VideoEncoder, quality, preset) -> dict: + metadataTags = {} + + if videoEncoder in (VideoEncoder.AV1, VideoEncoder.H264, VideoEncoder.VP9): + metadataTags["ENCODING_QUALITY"] = str(quality) + + if videoEncoder == VideoEncoder.AV1: + metadataTags["ENCODING_PRESET"] = str(preset) + + return metadataTags + + def generateAudioEncodingTokens(self): """Generates ffmpeg options audio streams including channel remapping, codec and bitrate""" @@ -261,6 +273,11 @@ class FfxController(): preset = presetFilters[0]['parameters']['preset'] if presetFilters else PresetFilter.DEFAULT_PRESET + self.__context['encoding_metadata_tags'] = self.generateEncodingMetadataTags( + videoEncoder, + quality, + preset, + ) filterParamTokens = [] diff --git a/src/ffx/media_descriptor_change_set.py b/src/ffx/media_descriptor_change_set.py index fdfaaf8..458259d 100644 --- a/src/ffx/media_descriptor_change_set.py +++ b/src/ffx/media_descriptor_change_set.py @@ -295,6 +295,9 @@ class MediaDescriptorChangeSet(): + f":{trackDescriptor.getSubIndex()}", f"{removeKey}="] + for tagKey, tagValue in self.__context.get('encoding_metadata_tags', {}).items(): + metadataTokens += [f"-metadata:g", f"{tagKey}={tagValue}"] + return metadataTokens diff --git a/tests/unit/test_ffx_controller.py b/tests/unit/test_ffx_controller.py new file mode 100644 index 0000000..197d818 --- /dev/null +++ b/tests/unit/test_ffx_controller.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + + +SRC_ROOT = Path(__file__).resolve().parents[2] / "src" + +if str(SRC_ROOT) not in sys.path: + sys.path.insert(0, str(SRC_ROOT)) + + +from ffx.ffx_controller import FfxController # noqa: E402 +from ffx.logging_utils import get_ffx_logger # noqa: E402 +from ffx.media_descriptor import MediaDescriptor # noqa: E402 +from ffx.track_codec import TrackCodec # noqa: E402 +from ffx.track_descriptor import TrackDescriptor # noqa: E402 +from ffx.track_type import TrackType # noqa: E402 +from ffx.video_encoder import VideoEncoder # noqa: E402 + + +class StaticConfig: + def __init__(self, data: dict | None = None): + self._data = data or {} + + def getData(self): + return self._data + + +class FfxControllerTests(unittest.TestCase): + def make_context(self, video_encoder: VideoEncoder) -> dict: + return { + "logger": get_ffx_logger(), + "config": StaticConfig(), + "video_encoder": video_encoder, + "dry_run": False, + "perform_cut": False, + "bitrates": { + "stereo": "112k", + "ac3": "256k", + "dts": "320k", + }, + } + + def make_media_descriptors(self) -> tuple[MediaDescriptor, MediaDescriptor]: + descriptor = MediaDescriptor( + track_descriptors=[ + TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.VIDEO, + codec_name=TrackCodec.H264, + ) + ] + ) + source_descriptor = MediaDescriptor( + track_descriptors=[ + TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.VIDEO, + codec_name=TrackCodec.H264, + ) + ] + ) + return descriptor, source_descriptor + + def test_vp9_run_job_emits_file_level_encoding_quality_metadata(self): + context = self.make_context(VideoEncoder.VP9) + target_descriptor, source_descriptor = self.make_media_descriptors() + controller = FfxController(context, target_descriptor, source_descriptor) + commands = [] + + with ( + patch.object( + controller, + "executeCommandSequence", + side_effect=lambda command: commands.append(command) or ("", "", 0), + ), + patch("ffx.ffx_controller.os.path.exists", return_value=False), + ): + controller.runJob( + "input.mkv", + "output.webm", + targetFormat="webm", + chainIteration=[ + { + "identifier": "quality", + "parameters": {"quality": 27}, + } + ], + ) + + self.assertEqual(2, len(commands)) + self.assertIn("-metadata:g", commands[1]) + self.assertIn("ENCODING_QUALITY=27", commands[1]) + self.assertFalse( + any(token.startswith("ENCODING_PRESET=") for token in commands[1]) + ) + + def test_av1_run_job_emits_file_level_quality_and_preset_metadata(self): + context = self.make_context(VideoEncoder.AV1) + target_descriptor, source_descriptor = self.make_media_descriptors() + controller = FfxController(context, target_descriptor, source_descriptor) + commands = [] + + with patch.object( + controller, + "executeCommandSequence", + side_effect=lambda command: commands.append(command) or ("", "", 0), + ): + controller.runJob( + "input.mkv", + "output.webm", + targetFormat="webm", + chainIteration=[ + { + "identifier": "quality", + "parameters": {"quality": 29}, + }, + { + "identifier": "preset", + "parameters": {"preset": 7}, + }, + ], + ) + + self.assertEqual(1, len(commands)) + self.assertIn("-metadata:g", commands[0]) + self.assertIn("ENCODING_QUALITY=29", commands[0]) + self.assertIn("ENCODING_PRESET=7", commands[0]) + + +if __name__ == "__main__": + unittest.main()