File size: 7,331 Bytes
9a069e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python3
"""Unit and CPU smoke checks for fixed-frame SegmentQA preprocessing."""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

import av

PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "scripts"))

import preprocess_segmentqa_fixed_frames as prep  # noqa: E402


PYTHON = "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/conda-envs/medrl/bin/python"
DATA_ROOT = "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge"
OUT = PROJECT_ROOT / "tmp/segmentqa_fixed80_preprocess_smoke_20260826"


def test_env() -> dict[str, str]:
    env = dict(os.environ)
    env["PYTHONPATH"] = str(PROJECT_ROOT / "ms-swift")
    return env


def video_stats(path: Path) -> dict:
    with av.open(str(path)) as container:
        stream = container.streams.video[0]
        pts = []
        dims = set()
        for frame in container.decode(stream):
            pts.append(float(frame.pts * frame.time_base))
            dims.add((frame.width, frame.height))
        return {"count": len(pts), "pts": pts, "dims": dims}


def test_time_and_sampling_helpers() -> None:
    assert prep.parse_time("01:02:03") == 3723
    assert prep.parse_time(12.5) == 12.5
    targets = prep.target_times(10, 18, 4)
    assert targets == [11, 13, 15, 17]
    assert prep.metadata_from_pts([10.1234, 10.5678], 420, 224) == {
        "fps": 1000,
        "frames_indices": [10123, 10568],
        "total_num_frames": 10569,
        "width": 420,
        "height": 224,
        "duration": 10.569,
        "video_backend": "precomputed_absolute_pts",
    }


def test_template_metadata_override_path() -> None:
    from swift.template.templates.qwen import _extract_precomputed_absolute_video_metadata as extract_metadata

    metadata = prep.metadata_from_pts([1.0, 2.0], 420, 224)
    video_inputs = {"video": "clip.mp4", "nframes": 80, "enable_thinking": False, "video_metadata": metadata}
    extracted = extract_metadata(video_inputs, 0)
    assert extracted == metadata
    assert video_inputs == {"video": "clip.mp4", "nframes": 80, "enable_thinking": False}
    legacy_inputs = {"video": "clip.mp4", "fps": 1.0, "min_frames": 16, "max_frames": 64}
    assert extract_metadata(legacy_inputs, 0) is None
    assert legacy_inputs == {"video": "clip.mp4", "fps": 1.0, "min_frames": 16, "max_frames": 64}


def validate_qwen_fetch_video_path(record: dict) -> None:
    from qwen_vl_utils import fetch_video
    from swift.template.templates.qwen import _extract_precomputed_absolute_video_metadata
    from transformers.video_utils import VideoMetadata

    video_inputs = {"video": record["videos"][0], **record["chat_template_kwargs"]}
    metadata = _extract_precomputed_absolute_video_metadata(video_inputs, 0)
    assert metadata is not None
    assert "do_sample_frames" not in metadata
    VideoMetadata(**metadata)
    (video, fetched_metadata), sample_fps = fetch_video(
        video_inputs, return_video_sample_fps=True, return_video_metadata=True, image_patch_size=16)
    assert video.shape[0] == 80
    assert video.shape[1] == 3
    assert video.shape[-2] % 32 == 0
    assert video.shape[-1] % 32 == 0
    assert fetched_metadata["total_num_frames"] >= 80
    assert sample_fps > 0
    assert video_inputs == {"video": record["videos"][0], "nframes": 80, "enable_thinking": False}


def assert_author_direct_segment_prompt(prompt: str) -> None:
    assert "timestamp in the source video" in prompt
    assert "<1234.5 seconds> is 00:20:34" in prompt
    assert "START OF THE VIDEO, not from the start of this" in prompt
    assert (
        "The foreign object classes are exactly: Sponge, Clip, Specimen Bag, Silicone Loop, External Drain, "
        "Needle, Gallstone, Specimen, Mesh, Absorbable Hemostatic Agent."
    ) in prompt
    assert "Reply with the answer and nothing else -- no reasoning, no preamble, no" in prompt
    assert "A single short line." in prompt


def test_cpu_smoke_preprocess_train() -> None:
    if OUT.exists():
        shutil.rmtree(OUT)
    cmd = [
        PYTHON,
        "scripts/preprocess_segmentqa_fixed_frames.py",
        "--data-root",
        DATA_ROOT,
        "--output-dir",
        str(OUT),
        "--splits",
        "train",
        "--frames",
        "80",
        "--width",
        "420",
        "--height",
        "224",
        "--workers",
        "1",
        "--limit",
        "2",
    ]
    result = subprocess.run(cmd, cwd=PROJECT_ROOT, check=True, text=True, capture_output=True, env=test_env())
    summary = json.loads(result.stdout.strip().splitlines()[-1])
    assert summary["segments"] == 2
    assert summary["success"] == 2
    assert summary["failed"] == 0

    manifest = [json.loads(line) for line in (OUT / "segment_manifest.jsonl").read_text().splitlines()]
    assert {item["dataset"] for item in manifest} == set(prep.DATASET_NAMES)
    for item in manifest:
        stats = video_stats(OUT / item["relpath"])
        assert stats["count"] == 80
        assert stats["dims"] == {(420, 224)}
        assert item["verified_frame_count"] == 80
        assert item["width"] == 420
        assert item["height"] == 224
        assert len(item["selected_source_pts"]) == 80
        assert all(item["start_seconds"] <= pts < item["end_seconds"] for pts in item["selected_source_pts"])
        assert item["video_metadata"]["fps"] == 1000
        assert item["video_metadata"]["frames_indices"] == [round(pts * 1000) for pts in item["selected_source_pts"]]
        assert item["video_metadata"]["total_num_frames"] == max(item["video_metadata"]["frames_indices"]) + 1
        assert item["video_metadata"]["video_backend"] == "precomputed_absolute_pts"
        assert "do_sample_frames" not in item["video_metadata"]

    train_rows = [json.loads(line) for line in (OUT / "train.jsonl").read_text().splitlines()]
    assert len(train_rows) >= 2
    assert all(row["messages"][0]["role"] == "system" for row in train_rows)
    assert_author_direct_segment_prompt(train_rows[0]["messages"][0]["content"])
    assert all(row["messages"][1]["content"].startswith("<video>\n") for row in train_rows)
    assert all(row["messages"][2]["role"] == "assistant" and row["messages"][2]["content"] for row in train_rows)
    assert all(row["chat_template_kwargs"]["nframes"] == 80 for row in train_rows)
    assert all(row["chat_template_kwargs"]["enable_thinking"] is False for row in train_rows)
    validate_qwen_fetch_video_path(train_rows[0])
    assert not (OUT / "test_requests.jsonl").exists()
    report = json.loads((OUT / "validation_report.json").read_text())
    assert report["status"] == "pass"
    assert report["success"] == report["segments"] == 2

    before = {p: p.stat().st_mtime for p in OUT.glob("videos/train/*/*.mp4")}
    resume = subprocess.run(cmd + ["--resume"], cwd=PROJECT_ROOT, check=True, text=True, capture_output=True, env=test_env())
    resume_summary = json.loads(resume.stdout.strip().splitlines()[-1])
    assert resume_summary["success"] == 2
    assert before == {p: p.stat().st_mtime for p in OUT.glob("videos/train/*/*.mp4")}


if __name__ == "__main__":
    test_time_and_sampling_helpers()
    test_template_metadata_override_path()
    test_cpu_smoke_preprocess_train()