File size: 2,513 Bytes
1e3ce4c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"""24fps compiler: keyframe cursors + AnimationSegments -> dense 24fps FrameStack.

The on-twos discipline (A1) is realized here. For each segment with stride=N, the
compiler advances to a new sample every N frames; intermediate frames hold the
prior sample. This is what makes the cursor's temporal layer load-bearing for
animation specifically — not decoration around a black-box video model.

This is real, runnable today on numpy alone. It alpha-blends keyframe artifacts
along eased curves; with a backend providing write_motion, the same compile path
fills the same FrameStack from model-generated samples instead.
"""
from __future__ import annotations
from typing import Iterable

import numpy as np

from pixel_cursor import PixelCursor, AnimationSegment, FrameStack, Image, interp_t
from pixel_cursor.artifact import _new_framestack


def compile_keyframes(
    keyframe_cursors: Iterable[PixelCursor],
    segments: Iterable[AnimationSegment],
    target_fps: int = 24,
) -> FrameStack:
    kf = list(keyframe_cursors)
    segs = list(segments)
    if len(kf) < 2:
        raise ValueError("Need at least 2 keyframe cursors")
    if len(segs) != len(kf) - 1:
        raise ValueError(
            f"Expected {len(kf) - 1} segments for {len(kf)} keyframes; got {len(segs)}"
        )
    for cur in kf:
        if not isinstance(cur.artifact, Image):
            raise TypeError(
                f"Keyframe cursors must point at Image artifacts; got {type(cur.artifact).__name__}"
            )

    total_frames = segs[-1].end_frame
    ref_pixels = kf[0].artifact.pixels
    h, w, c = ref_pixels.shape

    out = np.zeros((total_frames, h, w, c), dtype=np.uint8)

    for seg_idx, seg in enumerate(segs):
        a = kf[seg_idx].artifact.pixels
        b = kf[seg_idx + 1].artifact.pixels
        if a.shape != b.shape:
            raise ValueError(
                f"Segment {seg_idx}: keyframe shapes differ {a.shape} vs {b.shape}"
            )

        seg_len = seg.end_frame - seg.start_frame
        for f in range(seg.start_frame, seg.end_frame):
            rel = f - seg.start_frame
            sample_rel = (rel // seg.stride) * seg.stride
            denom = max(1, seg_len - 1)
            t = sample_rel / denom
            eased = interp_t(seg.curve_name, t)
            blended = (1.0 - eased) * a.astype(np.float32) + eased * b.astype(np.float32)
            out[f] = np.clip(blended, 0, 255).astype(np.uint8)

    return _new_framestack(out, fps=target_fps, name="compiled-24fps")