Files
modeling_zhaocui/deep_learning/Q1/q1/audit.py
T

247 lines
11 KiB
Python

from __future__ import annotations
import argparse
import csv
import json
import subprocess
from collections import Counter
from fractions import Fraction
from pathlib import Path
from typing import Any
from openpyxl import load_workbook
def _identifier(value: Any) -> str:
if value is None:
return ""
if isinstance(value, float) and value.is_integer():
return str(int(value))
return str(value).strip()
def _read_labels(path: Path) -> list[dict[str, Any]]:
workbook = load_workbook(path, read_only=True, data_only=True)
sheet = workbook.active
rows = sheet.iter_rows(values_only=True)
header = next(rows, None)
if header is None:
raise ValueError(f"empty workbook: {path}")
names = [str(value).strip().lower() if value is not None else "" for value in header]
required = ("video_id", "clip_id", "text", "label", "annotation")
missing = set(required) - set(names)
if missing:
raise ValueError(f"missing required label columns: {sorted(missing)}")
indexes = {name: names.index(name) for name in required}
records = []
for values in rows:
if not values or all(value is None for value in values):
continue
record = {name: values[index] if index < len(values) else None for name, index in indexes.items()}
record["video_id"] = _identifier(record["video_id"])
record["clip_id"] = _identifier(record["clip_id"])
record["text"] = "" if record["text"] is None else str(record["text"]).strip()
record["annotation"] = "" if record["annotation"] is None else str(record["annotation"]).strip()
if record["label"] is not None:
try:
record["label"] = float(record["label"])
except (TypeError, ValueError):
pass
records.append(record)
workbook.close()
return records
def _probe_video(path: Path) -> dict[str, Any]:
result = subprocess.run(
[
"ffprobe", "-v", "error", "-show_entries",
"format=duration:stream=codec_type,codec_name,width,height,avg_frame_rate,r_frame_rate,nb_frames,sample_rate,channels:frame=media_type,best_effort_timestamp_time,pkt_duration_time",
"-show_frames", "-of", "json", str(path),
],
check=True,
capture_output=True,
text=True,
)
payload = json.loads(result.stdout)
streams = payload.get("streams", [])
video = next((stream for stream in streams if stream.get("codec_type") == "video"), {})
audio = next((stream for stream in streams if stream.get("codec_type") == "audio"), {})
fps_text = video.get("avg_frame_rate") or video.get("r_frame_rate") or "0/1"
try:
fps = float(Fraction(fps_text))
except (ValueError, ZeroDivisionError):
fps = 0.0
container_duration = float(payload.get("format", {}).get("duration", 0.0))
frame_times: dict[str, list[tuple[float, float]]] = {"video": [], "audio": []}
for frame in payload.get("frames", []):
media_type = frame.get("media_type")
timestamp = frame.get("best_effort_timestamp_time")
if media_type not in frame_times or timestamp is None:
continue
try:
start = float(timestamp)
packet_duration = float(frame.get("pkt_duration_time", 0.0) or 0.0)
except (TypeError, ValueError):
continue
frame_times[media_type].append((start, packet_duration))
video_times = frame_times["video"]
audio_times = frame_times["audio"]
fallback_frame_duration = 1.0 / fps if fps > 0 else 0.0
video_start = min((item[0] for item in video_times), default=0.0)
video_end = max((start + (packet_duration or fallback_frame_duration) for start, packet_duration in video_times), default=container_duration)
audio_start = min((item[0] for item in audio_times), default=0.0)
audio_end = max((start + packet_duration for start, packet_duration in audio_times), default=container_duration)
decoded_duration = max(0.0, video_end - video_start)
audio_duration = max(0.0, audio_end - audio_start)
return {
"duration_s": decoded_duration or container_duration,
"container_duration_s": container_duration,
"audio_duration_s": audio_duration,
"video_timeline_start_s": video_start,
"video_timeline_end_s": video_end,
"audio_timeline_start_s": audio_start,
"audio_timeline_end_s": audio_end,
"video_codec": video.get("codec_name", ""),
"width": video.get("width", ""),
"height": video.get("height", ""),
"fps": fps,
"video_frames": len(video_times),
"container_video_frames": video.get("nb_frames", ""),
"audio_codec": audio.get("codec_name", ""),
"audio_sample_rate": audio.get("sample_rate", ""),
"audio_channels": audio.get("channels", ""),
"has_audio": bool(audio),
}
def audit_dataset(video_root: Path, label_file: Path, output_dir: Path, expected_count: int = 100) -> dict[str, Any]:
records = _read_labels(label_file)
videos: dict[tuple[str, str], Path] = {}
duplicate_video_files: list[str] = []
for video in sorted(video_root.rglob("*.mp4")):
key = (video.parent.name, video.stem)
if key in videos:
duplicate_video_files.append(str(video))
else:
videos[key] = video
keys = [(str(row["video_id"]), str(row["clip_id"])) for row in records]
counts = Counter(keys)
duplicate_rows = [list(key) for key, count in counts.items() if count > 1]
matched_keys = set(keys) & set(videos)
missing_keys = [key for key in keys if key not in videos]
extra_keys = sorted(set(videos) - set(keys))
class_mismatches = []
probe_errors: list[dict[str, str]] = []
probe_by_key: dict[tuple[str, str], dict[str, Any]] = {}
for key in matched_keys:
try:
probe_by_key[key] = _probe_video(videos[key])
except (OSError, subprocess.CalledProcessError, json.JSONDecodeError, ValueError) as error:
probe_errors.append({"video_id": key[0], "clip_id": key[1], "error": str(error)})
output_dir.mkdir(parents=True, exist_ok=True)
manifest_path = output_dir / "manifest.csv"
with manifest_path.open("w", encoding="utf-8-sig", newline="") as file:
writer = csv.DictWriter(
file,
fieldnames=(
"video_id", "clip_id", "group_id", "text", "label", "annotation", "video_path",
"video_exists", "duration_s", "container_duration_s", "audio_duration_s",
"video_timeline_start_s", "video_timeline_end_s", "audio_timeline_start_s",
"audio_timeline_end_s", "video_codec", "width", "height", "fps",
"video_frames", "container_video_frames", "audio_codec", "audio_sample_rate",
"audio_channels", "has_audio",
),
)
writer.writeheader()
for row, key in zip(records, keys):
label = row["label"]
annotation = str(row["annotation"]).strip().lower()
expected_class = "negative" if isinstance(label, (int, float)) and label < 0 else (
"positive" if isinstance(label, (int, float)) and label > 0 else "neutral"
)
if annotation in {"negative", "neutral", "positive"} and annotation != expected_class:
class_mismatches.append({"video_id": key[0], "clip_id": key[1], "label": label, "annotation": annotation})
path = videos.get(key)
probe = probe_by_key.get(key, {})
writer.writerow({
"video_id": key[0],
"clip_id": key[1],
"group_id": key[0],
"text": row["text"],
"label": row["label"],
"annotation": row["annotation"],
"video_path": str(path.relative_to(video_root)) if path else "",
"video_exists": bool(path),
**probe,
})
durations = [info["duration_s"] for info in probe_by_key.values() if info["duration_s"] > 0]
duration_out_of_range = [
{"video_id": key[0], "clip_id": key[1], "duration_s": info["duration_s"]}
for key, info in probe_by_key.items()
if not (2.648 <= info["duration_s"] <= 34.567)
]
audio_missing = [list(key) for key, info in probe_by_key.items() if not info["has_audio"]]
summary = {
"video_root": str(video_root),
"label_file": str(label_file),
"expected_count": expected_count,
"label_rows": len(records),
"unique_video_clip_pairs": len(set(keys)),
"unique_video_ids": len({key[0] for key in keys}),
"video_files": len(videos),
"matched_samples": len(matched_keys),
"coverage_rate": len(matched_keys) / max(len(records), 1),
"duration_min_s": min(durations) if durations else None,
"duration_max_s": max(durations) if durations else None,
"stated_duration_range_s": [2.648, 34.567],
"duration_out_of_range": duration_out_of_range,
"audio_stream_missing": audio_missing,
"media_probe_errors": probe_errors,
"missing_video_pairs": [list(key) for key in missing_keys],
"unlisted_video_pairs": [list(key) for key in extra_keys],
"duplicate_label_pairs": duplicate_rows,
"duplicate_video_files": duplicate_video_files,
"label_class_mismatches": class_mismatches,
"manifest": str(manifest_path),
}
summary["coverage_status"] = (
"complete_with_metadata_warnings" if duration_out_of_range else "complete"
)
summary_path = output_dir / "coverage_summary.json"
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
return summary
def main() -> int:
project_dir = Path(__file__).resolve().parents[1]
repo_dir = project_dir.parent
default_data = repo_dir / "E题数据" / "附件1-数据集原始多模态样本" / "MOSEI数据集部分原始视频-100条"
parser = argparse.ArgumentParser(description="Audit the 100 raw-video Q1 samples and export a manifest.")
parser.add_argument("--video-root", type=Path, default=default_data)
parser.add_argument("--labels", type=Path, default=default_data / "label-100.xlsx")
parser.add_argument("--output-dir", type=Path, default=project_dir / "outputs" / "audit")
parser.add_argument("--expected-count", type=int, default=100)
args = parser.parse_args()
summary = audit_dataset(args.video_root, args.labels, args.output_dir, args.expected_count)
print(json.dumps(summary, ensure_ascii=False, indent=2))
complete = (
summary["label_rows"] == args.expected_count
and summary["unique_video_clip_pairs"] == args.expected_count
and summary["matched_samples"] == args.expected_count
and not summary["duplicate_label_pairs"]
and not summary["label_class_mismatches"]
and not summary["audio_stream_missing"]
and not summary["media_probe_errors"]
)
return 0 if complete else 1
if __name__ == "__main__":
raise SystemExit(main())