Adding fix to objet_fit
This commit is contained in:
+20
-7
@@ -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", ""),
|
||||
|
||||
+3
-1
@@ -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}]"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user