Adding fix to objet_fit

This commit is contained in:
2026-08-04 14:31:18 +02:00
parent 0f3c595c04
commit 0b2ebf84e4
3 changed files with 72 additions and 8 deletions
+20 -7
View File
@@ -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: def parse_project_config(project_path: Path) -> ProjectConfig:
"""Parse project.json into ProjectConfig.""" """Parse project.json into ProjectConfig."""
config_path = project_path / "project.json" 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"), default_slide_type=data.get("defaultSlideType", "square"),
cutouts=cutouts, cutouts=cutouts,
default_filters=default_filters, default_filters=default_filters,
background=data.get("background", ""), background=_lc_handle(data.get("background", "")),
background_video=data.get("background_video", ""), # Deprecated background_video=data.get("background_video", ""), # Deprecated
slides_path=data.get("slides", "slides.json"), slides_path=data.get("slides", "slides.json"),
videos_path=data.get("videos", "videos.json"), videos_path=data.get("videos", "videos.json"),
audio_path=data.get("audio", "audio.json"), audio_path=data.get("audio", "audio.json"),
transcript_path=data.get("transcript"), transcript_path=data.get("transcript"),
audio_source=data.get("audio_source"), 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"), process_cache=data.get("process_cache"),
default_begin=float(data.get("default_begin", 0.0)), default_begin=float(data.get("default_begin", 0.0)),
default_end_trim=float(data.get("default_end_trim", 0.0)), default_end_trim=float(data.get("default_end_trim", 0.0)),
# Video handles are stored lowercased in videos.json (import lowercases the outro=_lc_handle(data.get("outro", [])),
# 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", [])],
description=data.get("description", ""), description=data.get("description", ""),
footer=data.get("footer", ""), footer=data.get("footer", ""),
output_video=data.get("output_video", ""), output_video=data.get("output_video", ""),
+3 -1
View File
@@ -1363,10 +1363,12 @@ def build_filter_complex(
# Scale and crop video # Scale and crop video
video_label = f"outro{i}" video_label = f"outro{i}"
start_pts = event.start_time start_pts = event.start_time
# OutroEvent carries no per-occurrence overrides, so read placement off
# its VideoSource (the videos.json defaults).
filters.append( filters.append(
f"[{video_idx}:v]format=yuva444p10le," f"[{video_idx}:v]format=yuva444p10le,"
f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB," 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}]" f"format=rgba[{video_label}]"
) )
+49
View File
@@ -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 any issues, raise ValidationError
if issues: if issues:
raise ValidationError(issues) raise ValidationError(issues)