---
title: metric
---

<div class="python-reference">
<a id="fastvideo.eval.metrics.audio.desync.metric-classes"></a>
<a id="fastvideo.eval.metrics.audio.desync.metric-functions"></a>
<a id="fastvideo.eval.metrics.audio.desync.metric"></a>
<p>Synchformer DeSync — average audio-video desynchronization in seconds.</p>
<p>Per-sample. Ports <code>hkchengrex/av-benchmark</code>'s <code>DeSync</code> against the
<code>24-01-04T16-39-21</code> Synchformer checkpoint (the one MMAudio reports
against). <code>argmax</code> over the 21-class grid in <code>[-2, +2]</code> seconds for
the first 14 and last 14 segments separately; lower is better, 0 = ideal.</p>
<p>Synchformer's transformer carries a fixed positional embedding sized for
~14 segments per modality, so keep input clips in the 3-10 s range —
shorter clips raise in <code>_segment_video</code> and longer clips ignore the
middle.</p>
<h2 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric</code></h2>
<pre class="doc-signature"><code>BaseMetric()</code></pre>
<p>Abstract base class for all eval metrics.</p>
<p>Two execution shapes:</p>
<ul>
<li>
<p><strong>Per-sample</strong> (<code>is_set_metric=False</code>, default) — implement
  :meth:<code>compute</code>. The Evaluator calls it once per input sample and
  returns one :class:<code>MetricResult</code> per sample.</p>
</li>
<li>
<p><strong>Set-vs-set</strong> (<code>is_set_metric=True</code>) — implement
  :meth:<code>accumulate</code> (called once per sample to buffer features)
  and :meth:<code>finalize</code> (called once after all samples to compute
  the corpus-level result). Use :meth:<code>reset</code> to clear buffers and
  :meth:<code>merge_from</code> to fold multi-GPU per-worker state together.</p>
</li>
</ul>
<p>Optionally override :meth:<code>setup</code> to eagerly load models. Metrics
that chunk along the time dim for memory hardcode their own chunk
size in <code>__init__</code> (see <code>optical_flow</code> for the canonical
example). Eval always processes one video per
:meth:<code>Evaluator.evaluate</code> call; <code>compute</code> / <code>accumulate</code>
receive a single sample, not a batch.</p>

<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.to"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.to</code></h3>
<pre class="doc-signature"><code>to(device: str | torch.device) -&gt; BaseMetric</code></pre>
<p>Move metric (and its internal models) to <em>device</em>.</p>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.setup"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.setup</code></h3>
<pre class="doc-signature"><code>setup() -&gt; None</code></pre>
<p>Eagerly load models. Called once by :class:<code>EvalWorker</code>.</p>
<p>Default is a no-op; metrics with no eager state (pixel math,
closed-form ops) inherit this. Override only if your metric
needs to load weights.</p>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.compute"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.compute</code></h3>
<pre class="doc-signature"><code>compute(sample: dict) -&gt; MetricResult</code></pre>
<p>Per-sample metrics: compute the score for one sample.</p>
<p><code>sample["video"]</code> is <code>(T, C, H, W)</code> float in <code>[0, 1]</code>.
<code>sample["reference"]</code> (if used) has the same shape. Return
<code>self._skip(sample, reason)</code> for missing inputs.</p>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.reset"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.reset</code></h3>
<pre class="doc-signature"><code>reset() -&gt; None</code></pre>
<p>Clear accumulator state at the start of each evaluate() call.</p>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.accumulate"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.accumulate</code></h3>
<pre class="doc-signature"><code>accumulate(sample: dict) -&gt; None</code></pre>
<p>Buffer per-sample features for a corpus-level metric.</p>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.finalize"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.finalize</code></h3>
<pre class="doc-signature"><code>finalize() -&gt; MetricResult</code></pre>
<p>Compute the corpus-level result from buffered state.</p>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.BaseMetric.merge_from"><code>fastvideo.eval.metrics.audio.desync.metric.BaseMetric.merge_from</code></h3>
<pre class="doc-signature"><code>merge_from(other: BaseMetric) -&gt; None</code></pre>
<p>Multi-GPU: fold another worker's accumulator state into this one.</p>
<h2 id="fastvideo.eval.metrics.audio.desync.metric.ensure_checkpoint"><code>fastvideo.eval.metrics.audio.desync.metric.ensure_checkpoint</code></h2>
<pre class="doc-signature"><code>ensure_checkpoint(name: str, source: str, filename: str | None = None) -&gt; str</code></pre>
<p>Resolve a model checkpoint path, downloading on miss.</p>
<p>See module docstring for the full source contract. <em>name</em> is used
only as the local cache filename for URL sources; ignored otherwise.</p>
<h2 id="fastvideo.eval.metrics.audio.desync.metric.register"><code>fastvideo.eval.metrics.audio.desync.metric.register</code></h2>
<pre class="doc-signature"><code>register(name: str)</code></pre>
<p>Decorator to register a metric class.</p>
<p>Usage::</p>
<pre><code>@register("ssim")
class SSIMMetric(BaseMetric):
    ...
</code></pre>
<h2 id="fastvideo.eval.metrics.audio.desync.metric.MetricResult"><code>fastvideo.eval.metrics.audio.desync.metric.MetricResult</code></h2>
<pre class="doc-signature"><code>MetricResult(name: str, score: float | None, details: dict&#91;str, Any&#93; = dict())</code></pre>
<p>Standard result container returned by all metrics.</p>
<p><code>score</code> is <code>None</code> when the metric was skipped (e.g. missing
required input).  Check <code>details["skipped"]</code> for the reason.</p>

<h2 id="fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric"><code>fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric</code></h2>
<pre class="doc-signature"><code>DeSyncMetric(src_fps: float | None = None)</code></pre>
<p>Per-sample Synchformer DeSync magnitude in seconds (lower is better).</p>

<details><summary>Source</summary><pre><code>@register(&quot;audio.desync&quot;)
class DeSyncMetric(BaseMetric):
    &quot;&quot;&quot;Per-sample Synchformer DeSync magnitude in seconds (lower is better).&quot;&quot;&quot;

    name = &quot;audio.desync&quot;
    requires_reference = False
    higher_is_better = False
    needs_gpu = True
    is_set_metric = False
    backbone = &quot;synchformer&quot;
    dependencies = &#91;&quot;librosa&quot;, &quot;torchaudio&quot;&#93;

    def __init__(self, src_fps: float | None = None) -&gt; None:
        super().__init__()
        # Defaults to 25 fps (Synchformer&#x27;s target) when the pool&#x27;s decode fps is unknown.
        self._src_fps = src_fps
        self._model: Any = None
        self._mel: Any = None
        self._grid: torch.Tensor | None = None
        self._resample_cache: dict&#91;int, Any&#93; = &#123;&#125;

    def to(self, device):
        super().to(device)
        if self._model is not None:
            self._model = self._model.to(self.device)
        if self._mel is not None:
            self._mel = self._mel.to(self.device)
        return self

    def setup(self) -&gt; None:
        if self._model is not None:
            return
        import torchaudio
        from fastvideo.third_party.eval.synchformer import Synchformer, make_class_grid

        ckpt = ensure_checkpoint(_SYNCHFORMER_NAME, _SYNCHFORMER_URL)
        model = Synchformer().to(self.device).eval()
        model.load_state_dict(torch.load(ckpt, weights_only=True, map_location=self.device))
        self._model = model

        self._mel = torchaudio.transforms.MelSpectrogram(
            sample_rate=_AUDIO_SR,
            win_length=400,
            hop_length=160,
            n_fft=1024,
            n_mels=128,
        ).to(self.device)
        self._grid = make_class_grid(_GRID_LOW, _GRID_HIGH, _GRID_SIZE)

    @torch.no_grad()
    def compute(self, sample: dict) -&gt; MetricResult:
        if self._model is None:
            self.setup()

        video = sample.get(&quot;video&quot;)
        audio_path = sample.get(&quot;audio&quot;)
        if video is None or audio_path is None:
            missing = &#91;k for k, v in ((&quot;video&quot;, video), (&quot;audio&quot;, audio_path)) if v is None&#93;
            return self._skip(sample, f&quot;missing &#123;&#x27;, &#x27;.join(repr(k) for k in missing)&#125;&quot;)

        # Prefer the per-sample fps the pool decoded at; fall back to the
        # constructor override. Without either, the audio/video windows can&#x27;t
        # be aligned, so skip rather than guess.
        sample_fps = sample.get(&quot;fps&quot;)
        src_fps = float(sample_fps) if sample_fps is not None else self._src_fps
        if src_fps is None:
            return self._skip(sample, &quot;missing &#x27;fps&#x27; (set sample fps or pass src_fps=)&quot;)

        # Video preprocessing → (S, T_seg, C, 224, 224)
        frames = video.float().to(self.device)
        frames = _resample_video(frames, _SYNC_FPS, src_fps)
        frames = _video_transform(frames)
        vsegs = _segment_video(frames).unsqueeze(0)  # (B=1, S, T_seg, C, H, W)
        # Synchformer&#x27;s extract_vfeats wants (B, S, T_seg, C, H, W); fold (B, S) → (B*S, 1, ...).
        b, s = vsegs.shape&#91;:2&#93;
        from einops import rearrange
        vsegs_for_model = rearrange(vsegs, &quot;b s t c h w -&gt; (b s) 1 t c h w&quot;)
        vfeats = self._model.extract_vfeats(vsegs_for_model)
        vfeats = rearrange(vfeats, &quot;(b s) 1 t d -&gt; b s t d&quot;, b=b)  # (1, S, t_v, D)

        # Audio preprocessing → Synchformer audio features (1, S, t_a, D)
        waveform = _load_audio_waveform(audio_path, self._resample_cache).to(self.device)
        assert self._mel is not None
        afeats = _encode_audio_segments(self._model, waveform, self._mel)

        # Compare first-14 and last-14 segments; argmax over the 21-class grid;
        # |grid|-value → average across the two directions.
        sync_grid = self._grid.to(self.device) if self._grid is not None else None
        assert sync_grid is not None
        # Synchformer&#x27;s transformer carries a fixed 198-token positional embedding
        # (~14 segments × (tv+ta) + 2 special tokens). Anything shorter would
        # shape-mismatch on the pos_emb add inside compare_v_a, so skip.
        n_vsegs = int(vfeats.shape&#91;1&#93;)
        n_asegs = int(afeats.shape&#91;1&#93;)
        s_used = min(n_vsegs, n_asegs)
        if s_used &lt; _NUM_SEG_PER_DIRECTION:
            return self._skip(
                sample,
                f&quot;too few segments for Synchformer pos_emb &quot;
                f&quot;(need &#123;_NUM_SEG_PER_DIRECTION&#125;, got v=&#123;n_vsegs&#125; a=&#123;n_asegs&#125;); &quot;
                f&quot;use clips of at least ~5 s&quot;,
            )
        s_used = _NUM_SEG_PER_DIRECTION
        front_logits = self._model.compare_v_a(vfeats&#91;:, :s_used&#93;, afeats&#91;:, :s_used&#93;)
        back_logits = self._model.compare_v_a(vfeats&#91;:, -s_used:&#93;, afeats&#91;:, -s_used:&#93;)
        front_id = int(torch.argmax(front_logits, dim=-1).item())
        back_id = int(torch.argmax(back_logits, dim=-1).item())
        front_d = abs(float(sync_grid&#91;front_id&#93;.item()))
        back_d = abs(float(sync_grid&#91;back_id&#93;.item()))
        desync = (front_d + back_d) / 2.0
        return MetricResult(
            name=self.name,
            score=desync,
            details=&#123;
                &quot;front_desync_s&quot;: front_d,
                &quot;back_desync_s&quot;: back_d,
                &quot;num_segments_used&quot;: s_used,
            &#125;,
        )</code></pre></details>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric.to"><code>fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric.to</code></h3>
<pre class="doc-signature"><code>to(device)</code></pre>

<details><summary>Source</summary><pre><code>def to(self, device):
    super().to(device)
    if self._model is not None:
        self._model = self._model.to(self.device)
    if self._mel is not None:
        self._mel = self._mel.to(self.device)
    return self</code></pre></details>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric.setup"><code>fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric.setup</code></h3>
<pre class="doc-signature"><code>setup() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>def setup(self) -&gt; None:
    if self._model is not None:
        return
    import torchaudio
    from fastvideo.third_party.eval.synchformer import Synchformer, make_class_grid

    ckpt = ensure_checkpoint(_SYNCHFORMER_NAME, _SYNCHFORMER_URL)
    model = Synchformer().to(self.device).eval()
    model.load_state_dict(torch.load(ckpt, weights_only=True, map_location=self.device))
    self._model = model

    self._mel = torchaudio.transforms.MelSpectrogram(
        sample_rate=_AUDIO_SR,
        win_length=400,
        hop_length=160,
        n_fft=1024,
        n_mels=128,
    ).to(self.device)
    self._grid = make_class_grid(_GRID_LOW, _GRID_HIGH, _GRID_SIZE)</code></pre></details>
<h3 id="fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric.compute"><code>fastvideo.eval.metrics.audio.desync.metric.DeSyncMetric.compute</code></h3>
<pre class="doc-signature"><code>compute(sample: dict) -&gt; MetricResult</code></pre>

<details><summary>Source</summary><pre><code>@torch.no_grad()
def compute(self, sample: dict) -&gt; MetricResult:
    if self._model is None:
        self.setup()

    video = sample.get(&quot;video&quot;)
    audio_path = sample.get(&quot;audio&quot;)
    if video is None or audio_path is None:
        missing = &#91;k for k, v in ((&quot;video&quot;, video), (&quot;audio&quot;, audio_path)) if v is None&#93;
        return self._skip(sample, f&quot;missing &#123;&#x27;, &#x27;.join(repr(k) for k in missing)&#125;&quot;)

    # Prefer the per-sample fps the pool decoded at; fall back to the
    # constructor override. Without either, the audio/video windows can&#x27;t
    # be aligned, so skip rather than guess.
    sample_fps = sample.get(&quot;fps&quot;)
    src_fps = float(sample_fps) if sample_fps is not None else self._src_fps
    if src_fps is None:
        return self._skip(sample, &quot;missing &#x27;fps&#x27; (set sample fps or pass src_fps=)&quot;)

    # Video preprocessing → (S, T_seg, C, 224, 224)
    frames = video.float().to(self.device)
    frames = _resample_video(frames, _SYNC_FPS, src_fps)
    frames = _video_transform(frames)
    vsegs = _segment_video(frames).unsqueeze(0)  # (B=1, S, T_seg, C, H, W)
    # Synchformer&#x27;s extract_vfeats wants (B, S, T_seg, C, H, W); fold (B, S) → (B*S, 1, ...).
    b, s = vsegs.shape&#91;:2&#93;
    from einops import rearrange
    vsegs_for_model = rearrange(vsegs, &quot;b s t c h w -&gt; (b s) 1 t c h w&quot;)
    vfeats = self._model.extract_vfeats(vsegs_for_model)
    vfeats = rearrange(vfeats, &quot;(b s) 1 t d -&gt; b s t d&quot;, b=b)  # (1, S, t_v, D)

    # Audio preprocessing → Synchformer audio features (1, S, t_a, D)
    waveform = _load_audio_waveform(audio_path, self._resample_cache).to(self.device)
    assert self._mel is not None
    afeats = _encode_audio_segments(self._model, waveform, self._mel)

    # Compare first-14 and last-14 segments; argmax over the 21-class grid;
    # |grid|-value → average across the two directions.
    sync_grid = self._grid.to(self.device) if self._grid is not None else None
    assert sync_grid is not None
    # Synchformer&#x27;s transformer carries a fixed 198-token positional embedding
    # (~14 segments × (tv+ta) + 2 special tokens). Anything shorter would
    # shape-mismatch on the pos_emb add inside compare_v_a, so skip.
    n_vsegs = int(vfeats.shape&#91;1&#93;)
    n_asegs = int(afeats.shape&#91;1&#93;)
    s_used = min(n_vsegs, n_asegs)
    if s_used &lt; _NUM_SEG_PER_DIRECTION:
        return self._skip(
            sample,
            f&quot;too few segments for Synchformer pos_emb &quot;
            f&quot;(need &#123;_NUM_SEG_PER_DIRECTION&#125;, got v=&#123;n_vsegs&#125; a=&#123;n_asegs&#125;); &quot;
            f&quot;use clips of at least ~5 s&quot;,
        )
    s_used = _NUM_SEG_PER_DIRECTION
    front_logits = self._model.compare_v_a(vfeats&#91;:, :s_used&#93;, afeats&#91;:, :s_used&#93;)
    back_logits = self._model.compare_v_a(vfeats&#91;:, -s_used:&#93;, afeats&#91;:, -s_used:&#93;)
    front_id = int(torch.argmax(front_logits, dim=-1).item())
    back_id = int(torch.argmax(back_logits, dim=-1).item())
    front_d = abs(float(sync_grid&#91;front_id&#93;.item()))
    back_d = abs(float(sync_grid&#91;back_id&#93;.item()))
    desync = (front_d + back_d) / 2.0
    return MetricResult(
        name=self.name,
        score=desync,
        details=&#123;
            &quot;front_desync_s&quot;: front_d,
            &quot;back_desync_s&quot;: back_d,
            &quot;num_segments_used&quot;: s_used,
        &#125;,
    )</code></pre></details>
</div>
