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:
|
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
@@ -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}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user