168 lines
5.7 KiB
Python
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()
|