| """Forward-Backward Reversal (FBR) data augmentation. |
| |
| Hypothesis: AI-generated forgery segments do not model temporal causality |
| (gravity, momentum, physical flow). Reversing the video makes real content |
| "look weird" but leaves the AI-segment's intrinsic artifacts intact. |
| |
| Implementation: with probability p, time-flip the cached video tensor and |
| remap each GT interval [s, e] -> [T - e, T - s] where T = duration. Train |
| with the standard iou reward. The model implicitly learns that forgery |
| detection should not depend on temporal direction — a useful inductive bias |
| when the data has no other signal pointing this way. |
| |
| This is the AUGMENTATION-form of FBC (the reward-form was validated to |
| have weak correlation r=0.196 on stage1_decomp_boundary; augmentation costs |
| no extra compute and creates additional training data with derived GT). |
| |
| Activation: |
| FORENSICS_FBR_AUG=true enables augmentation |
| FORENSICS_FBR_PROB=0.25 per-sample probability (default 0.25) |
| |
| Off by default; existing stage1 / v1 runs unaffected. Composable with SPI: |
| when both are enabled, sample is first FBR-flipped then SPI-shuffled (or |
| vice versa, doesn't matter for the math since flip and shuffle commute up |
| to GT remapping). |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import random |
| from typing import Any, Dict, List, Tuple |
|
|
| import torch |
|
|
|
|
| def _env_bool(name: str, default: str = "false") -> bool: |
| return os.getenv(name, default).lower() in ("true", "1", "yes") |
|
|
|
|
| def _normalise_intervals(solution: Any) -> List[Tuple[float, float]] | None: |
| if solution is None: |
| return None |
| if isinstance(solution, list) and solution: |
| first = solution[0] |
| if isinstance(first, (list, tuple)) and len(first) == 2 \ |
| and isinstance(first[0], (int, float)): |
| return [(float(s), float(e)) for s, e in solution] |
| if isinstance(first, (int, float)) and len(solution) == 2: |
| return [(float(solution[0]), float(solution[1]))] |
| return None |
|
|
|
|
| def maybe_apply_fbr(data: Dict[str, Any]) -> Dict[str, Any]: |
| if not _env_bool("FORENSICS_FBR_AUG"): |
| return data |
|
|
| prob = float(os.getenv("FORENSICS_FBR_PROB", "0.25")) |
| if random.random() > prob: |
| return data |
|
|
| use_pp = data.get("use_preprocessed", [False]) |
| if not (use_pp and use_pp[0]): |
| return data |
|
|
| try: |
| video_list = data["video_inputs"][0] |
| if not isinstance(video_list, list) or not video_list: |
| return data |
| video = video_list[0] |
| if not torch.is_tensor(video) or video.dim() < 3: |
| return data |
| T = video.shape[0] |
| if T < 8: |
| return data |
|
|
| kwargs = data["video_kwargs"][0] |
| fps = float(kwargs["fps"][0]) |
| if fps <= 0: |
| return data |
|
|
| intervals = _normalise_intervals(data.get("solution")) |
| if not intervals: |
| return data |
| except (KeyError, IndexError, TypeError, ValueError): |
| return data |
|
|
| duration = T / fps |
| new_video = video.flip(0).contiguous() |
| new_intervals = [] |
| for s, e in intervals: |
| |
| new_s = max(0.0, duration - e) |
| new_e = max(new_s + 1.0 / fps, duration - s) |
| new_intervals.append((new_s, new_e)) |
| new_intervals.sort() |
|
|
| data["video_inputs"] = [[new_video]] |
| data["solution"] = new_intervals |
| data["_fbr"] = [True] |
| return data |
|
|