feat(server): walk the replayed sample instead of freezing it

A still replayed as frames gave every frame the same boxes, so the live path
never saw a track move. /v1/samples/{id}?pan=N now returns a portrait 720x1280
window slid N*PAN_STEP pixels along the sample, reversing at each edge, at the
frame size and JPEG quality the phone sends.

The decoded image is cached by (mtime, size): re-reading a 1500x2000 JPEG per
frame cost more than the encode and made the walk slower than PAN_STEP says.
Measured after the cache: 3.3 ms mean per panned frame, 301 frames/s ceiling.

Two tests, both verified to fail before the change.
This commit is contained in:
kiyo9w 2026-09-29 09:28:07 +09:00
parent 4b00323ae2
commit 4ce41ecb17
3 changed files with 106 additions and 2 deletions

View file

@ -69,6 +69,30 @@ class RestContractTests(unittest.TestCase):
self.assertIn("version", body["model"])
self.assertEqual(body["catalog"]["sku_count"], 2)
def test_a_panned_sample_walks_and_stays_portrait(self):
# The debug replay source walks a shelf by asking for a frame index. A
# pan that returned the same window every time would freeze the stage
# while the boxes moved, which is the bug this pins.
first = self.client.get("/v1/samples/image6?pan=0")
self.assertEqual(first.status_code, 200)
self.assertEqual(first.headers["content-type"], "image/jpeg")
later = self.client.get("/v1/samples/image6?pan=40")
self.assertEqual(later.status_code, 200)
self.assertNotEqual(first.content, later.content, "a later pan must show a different window")
import cv2
import numpy as np
decoded = cv2.imdecode(np.frombuffer(later.content, dtype="uint8"), cv2.IMREAD_COLOR)
height, width = decoded.shape[:2]
self.assertEqual((width, height), (720, 1280), "the window must match the phone's portrait frame")
def test_a_sample_without_pan_is_the_whole_image(self):
response = self.client.get("/v1/samples/image6")
self.assertEqual(response.status_code, 200)
self.assertGreater(len(response.content), 1024)
def test_catalog_shape_has_no_local_paths(self):
body = self.client.get("/v1/catalog").json()
product = body["products"][0]