From 0b2ebf84e434529530b969b65be80c1a23cd8aba Mon Sep 17 00:00:00 2001 From: jenstandstad Date: Tue, 4 Aug 2026 14:31:18 +0200 Subject: [PATCH] Adding fix to objet_fit --- gnommo/parser.py | 27 ++++++++++++++++++------- gnommo/renderer.py | 4 +++- gnommo/validator.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/gnommo/parser.py b/gnommo/parser.py index accb514..9e18f29 100644 --- a/gnommo/parser.py +++ b/gnommo/parser.py @@ -236,6 +236,23 @@ def load_citations(path: Path) -> list[Citation]: ] +def _lc_handle(value): + """Lowercase a video handle (or list of handles) referenced in project.json. + + Video handles are stored lowercased as videos.json keys (import lowercases them), + so project.json references — outro, main_video, background — must be normalised to + match; otherwise a mixed-case entry like "OutroVideo6" fails the case-sensitive + lookup against key "outrovideo6" and the render reports it "not found" even though + it's there. The actual file path comes from the entry's source_file, so its case + is untouched. None / non-strings pass through unchanged. + """ + if isinstance(value, str): + return value.lower() + if isinstance(value, list): + return [x.lower() if isinstance(x, str) else x for x in value] + return value + + def parse_project_config(project_path: Path) -> ProjectConfig: """Parse project.json into ProjectConfig.""" config_path = project_path / "project.json" @@ -329,22 +346,18 @@ def parse_project_config(project_path: Path) -> ProjectConfig: default_slide_type=data.get("defaultSlideType", "square"), cutouts=cutouts, default_filters=default_filters, - background=data.get("background", ""), + background=_lc_handle(data.get("background", "")), background_video=data.get("background_video", ""), # Deprecated slides_path=data.get("slides", "slides.json"), videos_path=data.get("videos", "videos.json"), audio_path=data.get("audio", "audio.json"), transcript_path=data.get("transcript"), audio_source=data.get("audio_source"), - main_video=data.get("main_video"), + main_video=_lc_handle(data.get("main_video")), process_cache=data.get("process_cache"), default_begin=float(data.get("default_begin", 0.0)), default_end_trim=float(data.get("default_end_trim", 0.0)), - # Video handles are stored lowercased in videos.json (import lowercases the - # key), so normalise outro handles too — otherwise a project.json entry like - # "OutroVideo6" fails the case-sensitive lookup against key "outrovideo6" and - # the render reports it "not found in videos.json" even though it's there. - outro=[str(o).lower() for o in data.get("outro", [])], + outro=_lc_handle(data.get("outro", [])), description=data.get("description", ""), footer=data.get("footer", ""), output_video=data.get("output_video", ""), diff --git a/gnommo/renderer.py b/gnommo/renderer.py index cc5046b..b4265c6 100644 --- a/gnommo/renderer.py +++ b/gnommo/renderer.py @@ -1363,10 +1363,12 @@ def build_filter_complex( # Scale and crop video video_label = f"outro{i}" start_pts = event.start_time + # OutroEvent carries no per-occurrence overrides, so read placement off + # its VideoSource (the videos.json defaults). filters.append( f"[{video_idx}:v]format=yuva444p10le," f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB," - f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)}," + f"{_fit_filter(cut_width, cut_height, zoom, event.video_source.object_fit, event.video_source.object_position)}," f"format=rgba[{video_label}]" ) diff --git a/gnommo/validator.py b/gnommo/validator.py index c2a2456..c6bb63d 100644 --- a/gnommo/validator.py +++ b/gnommo/validator.py @@ -369,6 +369,55 @@ def validate_project( ) ) + # Validate presentation override VALUES so a typo — [vsb:x, object-fit=covfer], + # end_on=nextslide, layer=beneath — fails HERE instead of silently mis-rendering + # (or crashing) at render time. Checked case-insensitively against the value sets + # the renderer/transformer accept. Sources: inline manuscript overrides and the + # project's own videos.json entries. Keep end_on in sync with _extract_video_events. + import re as _re + from .parser import parse_marker + + _VALID_VALUES = { + "object-fit": {"cover", "contain"}, + "object-position": {"center", "top", "bottom", "left", "right"}, + "layer": {"above", "mid", "below"}, + "end_on": {"end", "loop", "next_slide", "slide", "next_video", + "video", "take", "end_marker"}, + } + + def _check_value(where: str, key: str, value, src_path: Path) -> None: + allowed = _VALID_VALUES.get(key) + if allowed is not None and value is not None and str(value).lower() not in allowed: + issues.append( + ValidationIssue( + f"{where}: invalid {key}={value!r} — valid values: {sorted(allowed)}", + src_path, + ) + ) + + # (a) inline manuscript overrides: [prefix:handle, key=value, …] + _mpath = project_path / "manuscript.txt" + if _mpath.exists(): + _mtext = _mpath.read_text(encoding="utf-8") + for _raw in _re.findall(r"\[([A-Za-z0-9_:./\-]+(?:,[^\]\n]*)?)\]", _mtext): + _mid, _overrides = parse_marker(_raw) + for _k, _v in (_overrides or {}).items(): + _check_value(f"[{_raw}]", _k, _v, _mpath) + + # (b) project videos.json entries (JSON keys mirror CSS: object-fit/object-position) + _vjson = project_path / config.videos_path + if _vjson.exists(): + try: + _raw_videos = _read_json(_vjson) + except Exception: + _raw_videos = {} + if isinstance(_raw_videos, dict): + for _vid, _entry in _raw_videos.items(): + if isinstance(_entry, dict): + for _k in ("object-fit", "object-position", "end_on", "layer"): + if _k in _entry: + _check_value(f"videos.json[{_vid}]", _k, _entry[_k], _vjson) + # If any issues, raise ValidationError if issues: raise ValidationError(issues)