Both players run from a video, and the middle chip finally plays you
Six sessions of asking an image model for a row of running figures, and it
never once drew a gait: three phases repeated with the same leg leading,
"ropeskipping, not running". A frame-by-frame LEFT/RIGHT skeleton spec, a
closing checklist, a loop clause, six figures instead of eight — all lost.
One 5-second image-to-video clip has no such problem, because it moves a
body instead of drawing poses. ~$0.42 a character through the key already in
.env, pose pinned at BOTH ends (pinned only at the front, she rotated into
profile halfway), stride period MEASURED off the frames rather than sampled
by eye — hers repeats in 6 clip frames, his in 23 — and the phases cut and
palette-snapped through _stride.cut. assets/_vidroll.py films and cuts it;
her first frame came from one still roll that took his approved pose as a
picture instead of a paragraph.
Wired: runN probes the sheet through SK() so the women's tour counts HER
frames, the player is no longer clamped to a two-frame pair, runHz 2.4 ->
1.9 ("this looks like roadrunner"), and the bench grew two PLAYER stops —
a cycle only ever seen mid-rally is a cycle nobody has watched.
Also in: the difficulty retune (medium is a real step above the shipped CPU
and is where `smart` first appears; hard stepped back off superhuman; easy
closed its gap) with MEDIUM's dial-for-dial pin to TUNE removed from the
test, and Oliver's own transparency fixes on the male frames.
SPRV 138 — the frames were right on disk and wrong on screen for an hour
because assets are cached for a year unless the ?v= moves. main.js v269.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -28,7 +28,9 @@ from PIL import Image
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SCRATCH = os.path.join(HERE, '_refcheck')
|
||||
MODEL = os.environ.get('SPLAT_MODEL', 'google/gemini-3-pro-image')
|
||||
N = 8 # frames per cycle — render.js probes run4/run8 and subdivides the same stride
|
||||
N = 6 # frames per cycle — render.js probes run4/run6/run8 and subdivides the same stride.
|
||||
# SIX, not eight: at eight the model pads the row with near-duplicates and the gait
|
||||
# blurs into a hop. Six is three phases and their mirror, which is what a run IS.
|
||||
TARGET_H = 224 # SPRITE_H: the cut anchors its tallest frame here, like every char sheet
|
||||
|
||||
# key -> identity frames (settled art, the character), install (prefix, suffix), who (words that
|
||||
@@ -38,17 +40,24 @@ CHARS = {
|
||||
'who': 'the LIVING male tennis player of image 1: neat brown hair under a white headband, '
|
||||
'a white polo shirt, blue shorts, white socks and white sneakers, tanned skin, '
|
||||
'carrying a wooden tennis racket',
|
||||
'view': 'his BACK to the camera in three-quarter view, the body angled toward the right, '
|
||||
'running to the RIGHT along the baseline — the right edge of his face just visible '
|
||||
'past his shoulder. NOT a straight-away back view: he moves ACROSS the frame, '
|
||||
'rightward, every figure leaning into that rightward run'},
|
||||
'view': 'SEEN FROM BEHIND AND ABOVE, running AWAY from the camera and off to the right — '
|
||||
'the camera stands behind the baseline looking down-court at his back, exactly the '
|
||||
'angle image 1 is drawn from. You see his shoulder blades, the back of his head '
|
||||
'and the soles of his shoes as they lift; only a sliver of cheek shows past the '
|
||||
'right shoulder. The body is turned about 30 degrees off straight-away so he '
|
||||
'travels diagonally away and to the RIGHT. A SIDE-ON PROFILE IS WRONG: if a figure '
|
||||
'shows its whole face, chest or a full profile silhouette, that figure is WRONG'},
|
||||
'pf': {'id': ['p_idle1_f', 'p_bounce_f', 'p_swing1_f', 'p_smash_f', 'p_toss_f'], 'install': ('p_run', '_f'),
|
||||
'who': 'the LIVING female tennis player of image 1: a dark ponytail, a white polo shirt, '
|
||||
'a white pleated tennis skirt, white socks and sneakers, carrying a wooden tennis racket',
|
||||
'view': 'her BACK to the camera in three-quarter view, the body angled toward the right, '
|
||||
'running to the RIGHT along the baseline — the right edge of her face just visible '
|
||||
'past her shoulder. NOT a straight-away back view: she moves ACROSS the frame, '
|
||||
'rightward, every figure leaning into that rightward run'},
|
||||
'view': 'SEEN FROM BEHIND AND ABOVE, running AWAY from the camera and off to the right — '
|
||||
'the camera stands behind the baseline looking down-court at her back, exactly the '
|
||||
'angle image 1 is drawn from. You see her shoulder blades, the ponytail swinging '
|
||||
'off the back of her head and the soles of her shoes as they lift; only a sliver '
|
||||
'of cheek shows past the right shoulder. The body is turned about 30 degrees off '
|
||||
'straight-away so she travels diagonally away and to the RIGHT. A SIDE-ON PROFILE '
|
||||
'IS WRONG: if a figure shows its whole face, chest or a full profile silhouette, '
|
||||
'that figure is WRONG'},
|
||||
'o': {'id': ['o_idle1', 'o_bounce', 'o_swing1', 'o_smash', 'o_toss'], 'install': ('o_run', ''),
|
||||
'who': 'the male tennis opponent of image 1: shaggy blond hair under a red headband, a red '
|
||||
'polo shirt with a dark collar, dark grey shorts, red socks, white sneakers, '
|
||||
@@ -90,10 +99,22 @@ def compose(k):
|
||||
|
||||
|
||||
def prompt(k):
|
||||
# THE BACKGROUND LINE COMES FIRST AND COMES FROM _regen.py (PROJECT.md's rule, re-learned
|
||||
# 2026-08-15): the back-view re-roll's freehand magenta prose lost to the camera change and the
|
||||
# sheet came back WHITE with a ghosted second row above the figures.
|
||||
sys.path.insert(0, HERE)
|
||||
from _regen import BG_FIRST
|
||||
c = CHARS[k]
|
||||
return (
|
||||
f'Draw a pixel-art sprite sheet: ONE ROW of EXACTLY {N} figures on a flat magenta background. '
|
||||
f'COUNT THEM: {N} figures, no fewer, no more — a row of 7 or 9 is WRONG.\n\n'
|
||||
BG_FIRST + '\n\n'
|
||||
f'Draw a pixel-art sprite sheet: ONE ROW of EXACTLY {N} figures on that flat magenta '
|
||||
f'background. COUNT THEM: {N} figures, no fewer, no more — a row of 7 or 9 is WRONG. ONE '
|
||||
'SINGLE HORIZONTAL ROW on one ground line: never a grid, never two rows, never a strip of '
|
||||
'extra heads or half-bodies above or below the row, and no faded, blurred, ghosted or '
|
||||
'onion-skinned figures anywhere on the canvas — every figure is fully drawn and fully opaque. '
|
||||
'THE FIGURES ARE SMALL IN THE FRAME: about two thirds of the canvas height, with a band of '
|
||||
'empty magenta roughly a sixth of the canvas wide above, below and beyond the outermost '
|
||||
'figure on each side.\n\n'
|
||||
f'IMAGE 1 is the ONLY source for who the character is: {c["who"]}. Copy the build, the '
|
||||
'garments, the hair and every one of its colours exactly. The poses in image 1 exist only to '
|
||||
'show the character — do NOT copy the poses.\n\n'
|
||||
@@ -103,7 +124,11 @@ def prompt(k):
|
||||
'behind the back to the left, never changes sides between neighbouring figures. The right '
|
||||
'arm swings a LITTLE; the racket head stays visibly right of the body\'s midline throughout. '
|
||||
'A single figure with the racket on the left side makes the whole animation flicker and is '
|
||||
'WRONG. THE INSIDE OF EVERY RACKET HEAD IS EXACTLY THE BACKGROUND MAGENTA #FF00FF — never '
|
||||
f'WRONG. BEFORE YOU FINISH, COUNT THE RACKETS: exactly {N}, one per figure — a row where one '
|
||||
'figure lost its racket is WRONG, and that is the exact defect this row replaces. Seen from '
|
||||
'behind the racket is FORESHORTENED and half-hidden by the hip — a short wooden shaft and an '
|
||||
'oval head angled away from the camera, never a broad flat paddle held out to the side. '
|
||||
'THE INSIDE OF EVERY RACKET HEAD IS EXACTLY THE BACKGROUND MAGENTA #FF00FF — never '
|
||||
'white, never grey, never solid: it cuts to transparency, the same rule the family sheets '
|
||||
'follow (a white-filled bed shipped as a solid paddle once — that is what this line prevents).\n\n'
|
||||
f'THE {N} FIGURES ARE ONE CONTINUOUS RUN CYCLE, {c["view"]}. Evenly spaced, every figure on '
|
||||
@@ -111,19 +136,47 @@ def prompt(k):
|
||||
'NEVER TOUCH OR OVERLAP: leave a clear band of empty magenta between every pair of '
|
||||
'neighbours — no part of any figure, the racket included, reaches into its neighbour\'s '
|
||||
'space. A racket overlapping the next figure is WRONG.\n\n'
|
||||
'The cycle, frame by frame: 1 the right foot planted under the body, left knee driving up and '
|
||||
'forward; 2 push-off, the right leg trailing straight behind, the left leg reaching forward; '
|
||||
'3 full flight, both feet off the ground, the stride at its widest — THE RACKET STILL '
|
||||
'GRIPPED IN THE RIGHT HAND in this airborne figure, exactly as in every other; 4 the left foot landing '
|
||||
'ahead of the body. Frames 5 to 8 are the SAME four moments with the legs swapped — the left '
|
||||
'foot planted, the right knee driving. Across the row the gap between the feet opens, closes, '
|
||||
'opens and closes again. The arms counter-swing: the opposite arm to the driving knee comes '
|
||||
'forward. If two neighbouring figures have the same leg forward, the row is WRONG.\n\n'
|
||||
# THE GAIT IS THE WHOLE JOB, and from behind it is carried by ONE readable fact per figure:
|
||||
# which shoe is flat on the ground and which sole is showing to the camera. Told as "the legs
|
||||
# alternate" the model drew a row of small symmetrical hops — Oliver: "the feet look rather
|
||||
# like ropeskipping than running". Named LEFT and RIGHT, figure by figure, it has something
|
||||
# to check itself against.
|
||||
'THE CYCLE, FIGURE BY FIGURE — name the left and right leg in each one and draw exactly this:\n'
|
||||
'1. The LEFT shoe is flat on the ground under the hips carrying the whole body; the RIGHT '
|
||||
'knee is driving up and FORWARD in front of the body, that thigh climbing so the right shoe '
|
||||
'is well clear of the ground. The body sits at its LOWEST here.\n'
|
||||
'2. Push-off: the LEFT leg snaps straight and trails BEHIND the body with the whole SOLE of '
|
||||
'the left shoe tipped up toward the camera, while the RIGHT foot reaches forward and lands '
|
||||
'ahead. The two shoes are far apart.\n'
|
||||
'3. Flight: BOTH feet are off the ground, the RIGHT foot forward and low about to take the '
|
||||
'weight, the LEFT foot high behind with its sole full to the camera. This is the WIDEST '
|
||||
'figure of the row and the body rides HIGHEST.\n'
|
||||
'4. The mirror of 1: the RIGHT shoe flat on the ground under the hips, the LEFT knee driving '
|
||||
'up and forward.\n'
|
||||
'5. The mirror of 2: the RIGHT leg straight and trailing behind, its sole to the camera, the '
|
||||
'LEFT foot reaching forward.\n'
|
||||
'6. The mirror of 3: flight again, LEFT foot forward and low, RIGHT foot high behind.\n'
|
||||
'IN EVERY SINGLE FIGURE ONE LEG IS CLEARLY AHEAD OF THE OTHER and the two shoes are at '
|
||||
'DIFFERENT heights. Two feet side by side under the hips, or two legs bent the same way, is '
|
||||
'SKIPPING WITH A ROPE, not running, and is WRONG. The leading leg must CHANGE SIDES from each '
|
||||
'figure to the next — never the same leg leading twice in a row, the last figure included, '
|
||||
'because the row is played as a loop. The hips and shoulders counter-rotate a little with '
|
||||
'each stride, and the free LEFT arm swings forward when the RIGHT knee drives.\n\n'
|
||||
'MATCH THE DRAWING STYLE OF IMAGE 1 EXACTLY. Image 1 is hard-edged pixel art and your output '
|
||||
'must be indistinguishable from it in technique: the same square pixel grid at the same pixel '
|
||||
'size, the same hard dark outline around every figure, the same flat blocks of colour, the '
|
||||
'same stepped aliased edges, the same hand-placed dithering. No soft brushwork, no blur, no '
|
||||
'gradients, no anti-aliasing.\n\n'
|
||||
'THIS IS A HARD SPRINT, NOT A JOG. At the widest figure the shoes are at least two shoe '
|
||||
'lengths apart and the torso leans forward over the leading knee; the trailing heel kicks up '
|
||||
'toward the backside. A row of figures standing upright with their feet close together is a '
|
||||
'WALK and is WRONG.\n\n'
|
||||
'EVERY FIGURE IS THE SAME SIZE as its neighbours and every figure stands on ONE shared '
|
||||
'horizontal ground line — the airborne figure lifts a little off it, nobody floats above the '
|
||||
'row.\n\n'
|
||||
f'LAST CHECK BEFORE YOU DRAW: walk the row figure by figure, 1 to {N}, and for each one name '
|
||||
'the racket in its right hand and the knee that leads. Every figure has a racket. No two '
|
||||
'neighbours lead with the same knee.\n\n'
|
||||
'The background is a solid, flat, uniform chroma key. Use EXACTLY hex color #FF00FF, one '
|
||||
'single flat tone across the whole canvas behind every figure, with no panels or boxes of any '
|
||||
'other colour. The inside of the racket head is that same EXACT #FF00FF. No text, no shadows, '
|
||||
|
||||
@@ -91,9 +91,9 @@ def one_ref(n, out, scale=4):
|
||||
|
||||
|
||||
PROMPT = (
|
||||
'IMAGE 1 is one figure from a zombie tennis game: a walking corpse holding a racket, drawn as '
|
||||
'hard-edged pixel art on a flat magenta background.\n\n'
|
||||
'Redraw IMAGE 1 with ONE change: THE LEGS ARE IN THE OPPOSITE PHASE OF THE SAME WALKING STEP. '
|
||||
'IMAGE 1 is one figure from a zombie tennis game — a player or a walking corpse, holding a '
|
||||
'racket, drawn as hard-edged pixel art on a flat magenta background.\n\n'
|
||||
'Redraw IMAGE 1 with ONE change: THE LEGS ARE IN THE OPPOSITE PHASE OF THE SAME STEP. '
|
||||
'The leg that is forward becomes the back leg and the leg that is back swings forward, so the two '
|
||||
'drawings together read as one body taking a step. The feet must clearly be in different places '
|
||||
'than in IMAGE 1.\n\n'
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# _vidroll.py — a run cycle out of a VIDEO model. One still frame in, one stride's worth of
|
||||
# sprites out.
|
||||
#
|
||||
# uv run --with pillow --with numpy --with requests python assets/_vidroll.py pf --film
|
||||
# uv run --with pillow --with numpy --with requests python assets/_vidroll.py pf --cut
|
||||
# uv run --with pillow --with numpy --with requests python assets/_vidroll.py pf --cut --install
|
||||
# keys: p (player m), pf (player f) [the opponents run two-frame pairs and do not need this]
|
||||
#
|
||||
# WHY THIS EXISTS, and why the still pipeline does not do it. `_runroll.py` asks one image model for
|
||||
# a ROW of run poses, and across six sessions it never once returned a real gait: it draws three
|
||||
# phases and repeats them with the same leg leading, which Oliver called "ropeskipping, not running"
|
||||
# — amplitude wiggle, no swap. Words did not fix it (a frame-by-frame LEFT/RIGHT skeleton spec, a
|
||||
# closing checklist, a loop clause: all of it lost). A VIDEO model has no such problem, because it
|
||||
# is not drawing poses, it is moving a body through time — the legs alternate because that is what
|
||||
# legs do. One 5-second clip is one guaranteed-coherent stride, and it costs about $0.42.
|
||||
#
|
||||
# THE RULES THAT MADE IT WORK (each one paid for with a bad clip):
|
||||
# * PIN THE POSE AT BOTH ENDS. first_frame AND last_frame set to the same still. Her first clip,
|
||||
# pinned only at the front, rotated her into a side profile by the second half.
|
||||
# * The prompt says the camera never moves and the runner never turns. Say both.
|
||||
# * MEASURE THE STRIDE, do not eyeball it: `--cut` scores every frame against every other with
|
||||
# _famref.legs/stride_diff and takes the period with the lowest self-difference, then samples
|
||||
# N phases evenly across ONE period. Her clip strides in 6 frames at 16 fps, his in 23 — a
|
||||
# fixed "every fifth frame" gave a cycle that read as a stagger.
|
||||
# * The frames go through `_stride.cut`, the same palette-snap-to-settled-art the leg-swap twin
|
||||
# uses. Raw clip frames are soft and their racket beds fill in; the cut is what makes them
|
||||
# sprites. Whole frames beat splicing: an earlier pass kept only the clip's legs under his own
|
||||
# drawn torso, and matching hips frame by frame was a week of seams (her skirt made it worse).
|
||||
#
|
||||
# THE CLIP'S FIRST FRAME IS THE WHOLE JOB. Generate it as a single still first — for her, one
|
||||
# `_pose_ref_run_*`-style identity reference plus HIS approved run frame as the pose picture ("she
|
||||
# is the person; his pose is the instruction"), which landed on roll one. A crisp first frame with
|
||||
# an open racket bed gives a clean clip; a soft one gives a clip whose racket wobbles.
|
||||
import base64, json, os, sys, time
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SCRATCH = os.path.join(HERE, '_refcheck')
|
||||
MODEL = os.environ.get('SPLAT_VIDEO_MODEL', 'kwaivgi/kling-v3.0-std')
|
||||
FPS = 16 # extraction rate: fine enough to find the period, coarse enough to score quickly
|
||||
N = 6 # phases installed — one stride, evenly sampled
|
||||
|
||||
CHARS = {
|
||||
'p': {'still': 'p_run1', 'like': 'p_idle1', 'install': ('p_run', ''), 'who': 'The man',
|
||||
'they': ('he', 'his', 'him')},
|
||||
'pf': {'still': 'p_run1_f', 'like': 'p_idle1_f', 'install': ('p_run', '_f'), 'who': 'The woman',
|
||||
'they': ('she', 'her', 'her')},
|
||||
}
|
||||
|
||||
|
||||
def key():
|
||||
if os.environ.get('OPENROUTER_API_KEY'): return os.environ['OPENROUTER_API_KEY']
|
||||
for line in open(os.path.join(HERE, '..', '.env')):
|
||||
if line.startswith('OPENROUTER_API_KEY='):
|
||||
return line.split('=', 1)[1].strip().strip('"').strip("'")
|
||||
sys.exit('no OPENROUTER_API_KEY')
|
||||
|
||||
|
||||
def prompt(k):
|
||||
c = CHARS[k]; s, poss, obj = c['they']
|
||||
return (
|
||||
f'Pixel-art video game sprite animation. {c["who"]} in the image keeps RUNNING AWAY from the '
|
||||
f'camera, seen from behind, travelling slightly to the right. Two full running strides: '
|
||||
f'{poss} legs alternate clearly, left knee driving up then right knee driving up, the trailing '
|
||||
f'foot kicking up behind {obj} each time. {s.capitalize()} grips the wooden tennis racket in '
|
||||
f'{poss} right hand the whole time and never lets go. {s.upper()} NEVER TURNS: the camera '
|
||||
f'stays behind {obj} for every single frame, {poss} face is never shown, {s} never rotates '
|
||||
f'into a side profile or toward the camera. THE CAMERA IS COMPLETELY STATIC — no pan, no zoom, '
|
||||
f'no dolly. {s.capitalize()} stays in the middle of the frame at the same size. The background '
|
||||
'stays ONE FLAT SOLID MAGENTA #FF00FF, empty — no court, no scenery, no shadow under '
|
||||
f'{poss} feet, no ground line, no contact shadow. The drawing style stays hard-edged pixel art '
|
||||
'the whole time.')
|
||||
|
||||
|
||||
def framed(src, out, size=(1024, 1024), scale=3):
|
||||
"""The sprite on flat magenta, big enough for a video model to see the pixels."""
|
||||
im = Image.open(src).convert('RGBA')
|
||||
im = im.resize((im.width * scale, im.height * scale), Image.NEAREST)
|
||||
c = Image.new('RGB', size, (255, 0, 255))
|
||||
c.paste(im, ((size[0] - im.width) // 2, size[1] - im.height - 60), im)
|
||||
c.save(out)
|
||||
return out
|
||||
|
||||
|
||||
def u(p):
|
||||
return 'data:image/png;base64,' + base64.b64encode(open(p, 'rb').read()).decode()
|
||||
|
||||
|
||||
def film(k):
|
||||
os.makedirs(SCRATCH, exist_ok=True)
|
||||
first = framed(os.path.join(HERE, CHARS[k]['still'] + '.png'), os.path.join(SCRATCH, f'vid_{k}_first.png'))
|
||||
img = {'type': 'image_url', 'image_url': {'url': u(first)}}
|
||||
r = requests.post('https://openrouter.ai/api/v1/videos',
|
||||
headers={'Authorization': f'Bearer {key()}', 'Content-Type': 'application/json'},
|
||||
json={'model': MODEL, 'prompt': prompt(k), 'duration': 5, 'aspect_ratio': '1:1',
|
||||
'generate_audio': False,
|
||||
# BOTH ends pinned to the same still: that is what holds the camera behind the runner
|
||||
'frame_images': [dict(img, frame_type='first_frame'), dict(img, frame_type='last_frame')]},
|
||||
timeout=600)
|
||||
if r.status_code >= 400: sys.exit(f'{k}: HTTP {r.status_code} {r.text[:300]}')
|
||||
job = r.json()
|
||||
poll = job.get('polling_url') or f"https://openrouter.ai/api/v1/videos/{job['id']}"
|
||||
for _ in range(120):
|
||||
time.sleep(10)
|
||||
p = requests.get(poll, headers={'Authorization': f'Bearer {key()}'}, timeout=120).json()
|
||||
print(' ', p.get('status'), flush=True)
|
||||
if p.get('status') in ('completed', 'succeeded', 'success'):
|
||||
url = (p.get('unsigned_urls') or [None])[0]
|
||||
# the "unsigned" url still wants the key — without it you save a 67-byte 401 body
|
||||
v = requests.get(url, headers={'Authorization': f'Bearer {key()}'}, timeout=600).content
|
||||
out = os.path.join(SCRATCH, f'vid_{k}.mp4')
|
||||
open(out, 'wb').write(v)
|
||||
print(f'{k}: {out} {len(v)} bytes cost {p.get("usage", {}).get("cost")}')
|
||||
return
|
||||
if p.get('status') in ('failed', 'error', 'cancelled'):
|
||||
sys.exit(json.dumps(p)[:400])
|
||||
sys.exit(f'{k}: still pending after 20 minutes')
|
||||
|
||||
|
||||
def cut(k, install=False):
|
||||
import subprocess
|
||||
sys.path.insert(0, HERE)
|
||||
import _stride
|
||||
from _famref import legs, stride_diff
|
||||
mp4 = os.path.join(SCRATCH, f'vid_{k}.mp4')
|
||||
if not os.path.exists(mp4): sys.exit(f'{mp4}: no clip — run `_vidroll.py {k} --film` first')
|
||||
d = os.path.join(SCRATCH, f'vidf_{k}')
|
||||
subprocess.run(['rm', '-rf', d], check=False); os.makedirs(d)
|
||||
subprocess.run(['ffmpeg', '-v', 'error', '-i', mp4, '-vf', f'fps={FPS}', f'{d}/f%03d.png'], check=True)
|
||||
fs = sorted(os.listdir(d))[6:46] # skip the held first frames
|
||||
ims = []
|
||||
for f in fs:
|
||||
im, err = _stride.cut(os.path.join(d, f), CHARS[k]['like'])
|
||||
if im: ims.append(im)
|
||||
if len(ims) < 12: sys.exit(f'{k}: only {len(ims)} frames survived the cut')
|
||||
L = [legs(i) for i in ims]
|
||||
# THE PERIOD IS MEASURED, NOT GUESSED: the lag whose frames look most like the frames one lag
|
||||
# earlier IS the stride. (A run that lopes and one that snaps differ by 4x — see the header.)
|
||||
scores = [(sum(stride_diff(L[i], L[i + p]) for i in range(len(L) - p)) / (len(L) - p), p)
|
||||
for p in range(4, min(26, len(L) - 2))]
|
||||
per = min(scores)[1]
|
||||
print(f'{k}: {len(ims)} frames, stride = {per} frames at {FPS} fps ({per / FPS:.2f} s)')
|
||||
sel = [round(i * per / N) for i in range(N)]
|
||||
pre, suf = CHARS[k]['install']
|
||||
outdir = HERE if install else os.path.join(SCRATCH, f'vidcut_{k}')
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
import numpy as np
|
||||
for n, i in enumerate(sel, 1):
|
||||
im = ims[i]
|
||||
# feet-centred like every other run cut: drawChar centres a frame by its total WIDTH, and
|
||||
# the racket's reach varies across a cycle, so an uncentred cycle slides sideways per frame
|
||||
a = np.array(im); op = a[..., 3] > 40
|
||||
ys, xs = np.nonzero(op[int(im.height * 0.66):])
|
||||
off = round(2 * (xs.mean() - im.width / 2)) if len(xs) else 0
|
||||
if off:
|
||||
p = Image.new('RGBA', (im.width + abs(off), im.height), (0, 0, 0, 0))
|
||||
p.paste(im, (0 if off > 0 else abs(off), 0)); im = p
|
||||
im.save(os.path.join(outdir, f'{pre}{n}{suf}.png'))
|
||||
print(f'{k}: {N} frames -> {outdir}' + ('' if install else ' (review, then --install)'))
|
||||
print(' BUMP SPRV in render.js, or the year-long asset cache serves the old frames.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('-')]
|
||||
if len(args) != 1 or args[0] not in CHARS:
|
||||
sys.exit(f'usage: _vidroll.py <key> [--film|--cut] [--install] keys: {" ".join(CHARS)}')
|
||||
k = args[0]
|
||||
if '--film' in sys.argv: film(k)
|
||||
elif '--cut' in sys.argv: cut(k, install='--install' in sys.argv)
|
||||
else: sys.exit('pass --film or --cut')
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |