#!/usr/bin/env python # Copyright 2024 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging from pathlib import Path from typing import Any logger = logging.getLogger(__name__) JsonLike = str | int | float | bool | None | list["JsonLike"] | dict[str, "JsonLike"] | tuple["JsonLike", ...] def load_json(fpath: Path) -> Any: """Load data from a JSON file. Args: fpath (Path): Path to the JSON file. Returns: Any: The data loaded from the JSON file. """ with open(fpath) as f: return json.load(f) def write_json(data: dict, fpath: Path) -> None: """Write data to a JSON file. Creates parent directories if they don't exist. Args: data (dict): The dictionary to write. fpath (Path): The path to the output JSON file. """ fpath.parent.mkdir(exist_ok=True, parents=True) with open(fpath, "w") as f: json.dump(data, f, indent=4, ensure_ascii=False) def write_video(video_path: str | Path, stacked_frames: list, fps: int) -> None: """Write a sequence of RGB frames to an MP4 video file using libx264. Args: video_path: Output file path. stacked_frames: List of HWC uint8 numpy arrays (RGB). fps: Frames per second for the output video. """ from .import_utils import require_package require_package("av", extra="av-dep") import av with av.open(str(video_path), mode="w") as container: orig_height, orig_width = stacked_frames[0].shape[:2] # yuv420p requires even dimensions; crop by one pixel if needed height = orig_height if orig_height % 2 == 0 else orig_height - 1 width = orig_width if orig_width % 2 == 0 else orig_width - 1 if height != orig_height or width != orig_width: logger.warning( "Frame dimensions %dx%d are not even; cropping to %dx%d for yuv420p compatibility.", orig_width, orig_height, width, height, ) stream = container.add_stream("libx264", rate=fps) stream.width = width stream.height = height stream.pix_fmt = "yuv420p" for frame_array in stacked_frames: if height != orig_height or width != orig_width: frame_array = frame_array[:height, :width] frame = av.VideoFrame.from_ndarray(frame_array, format="rgb24") for packet in stream.encode(frame): container.mux(packet) for packet in stream.encode(): container.mux(packet) def deserialize_json_into_object[T: JsonLike](fpath: Path, obj: T) -> T: """ Loads the JSON data from `fpath` and recursively fills `obj` with the corresponding values (strictly matching structure and types). Tuples in `obj` are expected to be lists in the JSON data, which will be converted back into tuples. """ with open(fpath, encoding="utf-8") as f: data = json.load(f) def _deserialize(target, source): """ Recursively overwrite the structure in `target` with data from `source`, performing strict checks on structure and type. Returns the updated version of `target` (especially important for tuples). """ # If the target is a dictionary, source must be a dictionary as well. if isinstance(target, dict): if not isinstance(source, dict): raise TypeError(f"Type mismatch: expected dict, got {type(source)}") # Check that they have exactly the same set of keys. if target.keys() != source.keys(): raise ValueError( f"Dictionary keys do not match.\nExpected: {target.keys()}, got: {source.keys()}" ) # Recursively update each key. for k in target: target[k] = _deserialize(target[k], source[k]) return target # If the target is a list, source must be a list as well. elif isinstance(target, list): if not isinstance(source, list): raise TypeError(f"Type mismatch: expected list, got {type(source)}") # Check length if len(target) != len(source): raise ValueError(f"List length mismatch: expected {len(target)}, got {len(source)}") # Recursively update each element. for i in range(len(target)): target[i] = _deserialize(target[i], source[i]) return target # If the target is a tuple, the source must be a list in JSON, # which we'll convert back to a tuple. elif isinstance(target, tuple): if not isinstance(source, list): raise TypeError(f"Type mismatch: expected list (for tuple), got {type(source)}") if len(target) != len(source): raise ValueError(f"Tuple length mismatch: expected {len(target)}, got {len(source)}") # Convert each element, forming a new tuple. converted_items = [] for t_item, s_item in zip(target, source, strict=False): converted_items.append(_deserialize(t_item, s_item)) # Return a brand new tuple (tuples are immutable in Python). return tuple(converted_items) # Otherwise, we're dealing with a "primitive" (int, float, str, bool, None). else: # Check the exact type. If these must match 1:1, do: if type(target) is not type(source): raise TypeError(f"Type mismatch: expected {type(target)}, got {type(source)}") return source # Perform the in-place/recursive deserialization updated_obj = _deserialize(obj, data) return updated_obj