Files
video-ai-analysis/tests/test_clips.py
2026-06-17 11:33:54 +08:00

168 lines
5.7 KiB
Python

import json
import tempfile
import unittest
from pathlib import Path
from video_ai_analysis_poc.clips import build_clip_records, build_clip_records_from_manifest
class ClipTests(unittest.TestCase):
def test_build_clip_records_uniformly_samples_frames_per_clip(self):
frames = [
{
"video_id": "video-abc",
"frame_id": f"video-abc_f{index + 1:06d}",
"frame_path": f"frames/video-abc/{index + 1:06d}.jpg",
"offset_seconds": float(index),
"timecode": f"00:00:{index:02d}",
"pts_time": float(index),
"status": "sampled",
}
for index in range(10)
]
clips = build_clip_records(
frames,
{
"length_seconds": 10,
"stride_seconds": 10,
"frames_per_clip": 4,
"min_frames_per_clip": 2,
},
)
self.assertEqual(len(clips), 1)
self.assertEqual(clips[0]["clip_id"], "video-abc_c000001")
self.assertEqual(clips[0]["clip_start_seconds"], 0.0)
self.assertEqual(clips[0]["clip_end_seconds"], 10.0)
self.assertEqual(
[frame["offset_seconds"] for frame in clips[0]["frame_times"]],
[0.0, 3.0, 6.0, 9.0],
)
self.assertEqual(clips[0]["status"], "pending")
self.assertEqual(clips[0]["retry_count"], 0)
self.assertIsNone(clips[0]["last_error"])
def test_tail_clip_end_is_truncated_to_last_frame_interval(self):
frames = [
{
"video_id": "video-abc",
"frame_id": f"video-abc_f{index + 1:06d}",
"frame_path": f"frames/video-abc/{index + 1:06d}.jpg",
"offset_seconds": float(index),
"timecode": f"00:00:{index:02d}",
"pts_time": float(index),
"status": "sampled",
}
for index in range(15)
]
clips = build_clip_records(
frames,
{
"length_seconds": 10,
"stride_seconds": 10,
"frames_per_clip": 8,
"min_frames_per_clip": 4,
},
)
self.assertEqual(len(clips), 2)
self.assertEqual(clips[1]["clip_start_seconds"], 10.0)
self.assertEqual(clips[1]["clip_end_seconds"], 15.0)
self.assertEqual(clips[1]["clip_end_timecode"], "00:00:15")
def test_build_clip_records_adds_beijing_time_range_and_frame_times(self):
frames = [
{
"video_id": "video-abc",
"frame_id": f"video-abc_f{index + 1:06d}",
"frame_path": f"frames/video-abc/{index + 1:06d}.jpg",
"offset_seconds": float(index),
"timecode": f"00:00:{index:02d}",
"pts_time": float(index),
"beijing_time": f"2026-06-15 07:00:{index:02d}",
"status": "sampled",
}
for index in range(10)
]
clips = build_clip_records(
frames,
{
"length_seconds": 10,
"stride_seconds": 10,
"frames_per_clip": 4,
"min_frames_per_clip": 2,
},
)
self.assertEqual(clips[0]["clip_start_beijing_time"], "2026-06-15 07:00:00")
self.assertEqual(clips[0]["clip_end_beijing_time"], "2026-06-15 07:00:10")
self.assertEqual(
[frame["beijing_time"] for frame in clips[0]["frame_times"]],
[
"2026-06-15 07:00:00",
"2026-06-15 07:00:03",
"2026-06-15 07:00:06",
"2026-06-15 07:00:09",
],
)
def test_build_clip_records_from_manifest_skips_failed_frames_and_writes_jsonl(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
frame_manifest = root / "frame_manifest.jsonl"
clip_manifest = root / "clip_manifest.jsonl"
records = [
{
"video_id": "video-abc",
"frame_id": f"video-abc_f{index + 1:06d}",
"frame_path": f"frames/video-abc/{index + 1:06d}.jpg",
"offset_seconds": float(index),
"timecode": f"00:00:{index:02d}",
"pts_time": float(index),
"status": "sampled",
}
for index in range(4)
]
records.append(
{
"video_id": "video-abc",
"frame_id": None,
"frame_path": None,
"offset_seconds": None,
"timecode": None,
"pts_time": None,
"status": "sample_failed",
"last_error": "bad decode",
}
)
frame_manifest.write_text(
"\n".join(json.dumps(record, sort_keys=True) for record in records) + "\n",
encoding="utf-8",
)
clips = build_clip_records_from_manifest(
frame_manifest,
clip_manifest,
{
"length_seconds": 10,
"stride_seconds": 10,
"frames_per_clip": 8,
"min_frames_per_clip": 4,
},
)
self.assertEqual(len(clips), 1)
self.assertEqual(len(clips[0]["frame_times"]), 4)
persisted = [
json.loads(line)
for line in clip_manifest.read_text(encoding="utf-8").splitlines()
]
self.assertEqual(persisted, clips)
if __name__ == "__main__":
unittest.main()