---
title: metric
---

<div class="python-reference">
<a id="fastvideo.eval.metrics.audio.wer.metric-classes"></a>
<a id="fastvideo.eval.metrics.audio.wer.metric-functions"></a>
<a id="fastvideo.eval.metrics.audio.wer.metric"></a>
<p>Word Error Rate for generated audio.</p>
<p>Per-sample. Default backend is Whisper-base; <code>glm_asr</code> (vendored under
<code>third_party/eval/glmasr/</code>) and <code>sensevoice</code> (FunASR, install-on-
demand) are selectable via <code>asr_backend=</code>. Text is normalized per the
MagiHuman convention and CJK inputs are scored at the character level.</p>
<h2 id="fastvideo.eval.metrics.audio.wer.metric.BaseMetric"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.to"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.setup"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.compute"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.reset"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.accumulate"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.finalize"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.BaseMetric.merge_from"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.register"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.MetricResult"><code>fastvideo.eval.metrics.audio.wer.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.wer.metric.WERMetric"><code>fastvideo.eval.metrics.audio.wer.metric.WERMetric</code></h2>
<pre class="doc-signature"><code>WERMetric(asr_backend: str = &#x27;whisper&#x27;, model_name: str | None = None, instruction: str = &#x27;Please transcribe this audio into text&#x27;)</code></pre>
<p>WER metric with selectable ASR backend.</p>
<p>Sample fields: <code>audio</code>, <code>reference_text</code> (or <code>text_prompt</code>),
optional <code>language</code>. <code>instruction</code> is passed to GLM-ASR.</p>

<details><summary>Source</summary><pre><code>@register(&quot;audio.wer&quot;)
class WERMetric(BaseMetric):
    &quot;&quot;&quot;WER metric with selectable ASR backend.

    Sample fields: ``audio``, ``reference_text`` (or ``text_prompt``),
    optional ``language``. ``instruction`` is passed to GLM-ASR.
    &quot;&quot;&quot;

    name = &quot;audio.wer&quot;
    requires_reference = False
    higher_is_better = False
    needs_gpu = True
    is_set_metric = False
    dependencies = &#91;&quot;jiwer&quot;, &quot;librosa&quot;&#93;

    def __init__(
        self,
        asr_backend: str = &quot;whisper&quot;,
        model_name: str | None = None,
        instruction: str = &quot;Please transcribe this audio into text&quot;,
    ) -&gt; None:
        super().__init__()
        if asr_backend not in (&quot;whisper&quot;, &quot;glm_asr&quot;, &quot;sensevoice&quot;):
            raise ValueError(f&quot;Unknown ASR backend &#x27;&#123;asr_backend&#125;&#x27;. &quot;
                             &quot;Supported: &#91;&#x27;whisper&#x27;, &#x27;glm_asr&#x27;, &#x27;sensevoice&#x27;&#93;&quot;)
        self._asr_backend = asr_backend
        self._model_name = model_name
        self._instruction = instruction
        self._model: Any = None
        self._processor: Any = None

    def to(self, device):
        super().to(device)
        return self

    def setup(self) -&gt; None:
        if self._model is not None:
            return
        if self._asr_backend == &quot;whisper&quot;:
            self._setup_whisper()
        elif self._asr_backend == &quot;glm_asr&quot;:
            self._setup_glm_asr()
        else:
            self._setup_sensevoice()

    def _setup_whisper(self) -&gt; None:
        from transformers import WhisperForConditionalGeneration, WhisperProcessor

        repo_id = self._model_name or &quot;openai/whisper-base&quot;
        dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
        self._processor = WhisperProcessor.from_pretrained(repo_id)
        self._model = WhisperForConditionalGeneration.from_pretrained(repo_id, torch_dtype=dtype).to(self.device)
        self._model.eval()

    def _setup_glm_asr(self) -&gt; None:
        # transformers ≤ 4.57 doesn&#x27;t register ``model_type=glmasr`` and
        # the HF repo ships no remote modeling code; use our vendored copy.
        from fastvideo.third_party.eval.glmasr import register_with_auto
        register_with_auto()

        from transformers import AutoModel, AutoProcessor

        repo_id = self._model_name or &quot;zai-org/GLM-ASR-Nano-2512&quot;
        dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
        self._processor = AutoProcessor.from_pretrained(repo_id)
        self._model = AutoModel.from_pretrained(repo_id, torch_dtype=dtype)
        self._model.to(self.device)
        self._model.eval()

    def _setup_sensevoice(self) -&gt; None:
        try:
            from funasr import AutoModel
        except ImportError as e:
            raise ImportError(&quot;SenseVoice backend requires `funasr`. Install it or &quot;
                              &quot;switch to asr_backend=&#x27;glm_asr&#x27;.&quot;) from e

        model_name = self._model_name or &quot;FunAudioLLM/SenseVoiceSmall&quot;
        self._model = AutoModel(
            model=model_name,
            vad_model=&quot;fsmn-vad&quot;,
            vad_kwargs=&#123;&quot;max_single_segment_time&quot;: 30000&#125;,
            device=str(self.device),
            hub=&quot;hf&quot;,
            disable_update=True,
        )

    def _move_to_device(self, inputs: Any) -&gt; Any:
        &quot;&quot;&quot;Move processor outputs to device, casting only floats to model dtype.

        ``BatchFeature.to(device)`` moves tensors but doesn&#x27;t cast floats,
        so log-mel ``input_features`` (float32) would hit a bf16 model.
        &quot;&quot;&quot;
        try:
            model_dtype = next(self._model.parameters()).dtype
        except StopIteration:
            model_dtype = None

        if hasattr(inputs, &quot;items&quot;):
            moved: dict&#91;str, Any&#93; = &#123;&#125;
            for k, v in inputs.items():
                if torch.is_tensor(v):
                    v = v.to(self.device)
                    if model_dtype is not None and torch.is_floating_point(v):
                        v = v.to(model_dtype)
                moved&#91;k&#93; = v
            return moved
        return inputs.to(self.device) if hasattr(inputs, &quot;to&quot;) else inputs

    def _transcribe_glm_asr(self, audio_path: str) -&gt; str:
        # transformers ≤ 4.57&#x27;s ``apply_chat_template`` doesn&#x27;t auto-extract
        # audio features, and the repo&#x27;s chat template emits a stray
        # ``&lt;|user|&gt;`` token that breaks generate. Build the prompt by hand
        # and call ``processor.__call__`` directly so it expands the
        # ``&lt;|pad|&gt;`` placeholder and extracts log-mel features in one pass.
        import librosa
        prompt = (f&quot;&lt;|user|&gt;\n&quot;
                  f&quot;&lt;|begin_of_audio|&gt;&#123;self._processor.audio_token&#125;&lt;|end_of_audio|&gt;\n&quot;
                  f&quot;&#123;self._instruction&#125;\n&quot;
                  f&quot;&lt;|assistant|&gt;\n&quot;)
        audio, _ = librosa.load(audio_path, sr=16000, mono=True)
        inputs = self._processor(text=prompt, audio=audio, return_tensors=&quot;pt&quot;)
        inputs = self._move_to_device(inputs)
        with torch.no_grad():
            outputs = self._model.generate(**inputs, max_new_tokens=256, do_sample=False)
        prompt_len = inputs&#91;&quot;input_ids&quot;&#93;.shape&#91;1&#93;
        return self._processor.batch_decode(outputs&#91;:, prompt_len:&#93;, skip_special_tokens=True)&#91;0&#93;.strip()

    def _transcribe_whisper(self, audio_path: str) -&gt; str:
        import librosa
        audio, _ = librosa.load(audio_path, sr=16000, mono=True)
        inputs = self._processor(audio, sampling_rate=16000, return_tensors=&quot;pt&quot;)
        input_features = inputs.input_features.to(self.device)
        if torch.is_floating_point(input_features):
            input_features = input_features.to(next(self._model.parameters()).dtype)
        with torch.no_grad():
            ids = self._model.generate(input_features=input_features, max_new_tokens=256)
        return self._processor.batch_decode(ids, skip_special_tokens=True)&#91;0&#93;.strip()

    def _transcribe_sensevoice(self, audio_path: str) -&gt; str:
        from funasr.utils.postprocess_utils import rich_transcription_postprocess
        res = self._model.generate(
            input=audio_path,
            cache=&#123;&#125;,
            language=&quot;auto&quot;,
            use_itn=True,
            batch_size_s=60,
            merge_vad=True,
            merge_length_s=15,
        )
        return rich_transcription_postprocess(res&#91;0&#93;&#91;&quot;text&quot;&#93;).strip()

    def _transcribe(self, audio_path: str) -&gt; str:
        if self._asr_backend == &quot;whisper&quot;:
            return self._transcribe_whisper(audio_path)
        if self._asr_backend == &quot;glm_asr&quot;:
            return self._transcribe_glm_asr(audio_path)
        return self._transcribe_sensevoice(audio_path)

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

        audio = sample.get(&quot;audio&quot;)
        if audio is None:
            return self._skip(sample, &quot;missing &#x27;audio&#x27;&quot;)
        text = sample.get(&quot;reference_text&quot;, sample.get(&quot;text_prompt&quot;))
        if text is None:
            return self._skip(sample, &quot;missing reference_text/text_prompt&quot;)
        language = sample.get(&quot;language&quot;)

        import jiwer

        asr = self._transcribe(audio)
        if set(asr).issubset(PUNCTUATION_SET):
            asr = &quot;&quot;

        force_char_level = _resolve_char_level(language)
        ref_for_wer, hyp_for_wer, char_level = _prepare_for_wer(text, asr, force_char_level)
        wer = jiwer.wer(ref_for_wer, hyp_for_wer)

        return MetricResult(
            name=self.name,
            score=float(wer),
            details=&#123;
                &quot;transcription&quot;: _normalize_text(asr),
                &quot;reference_text&quot;: _normalize_text(text),
                &quot;asr_backend&quot;: self._asr_backend,
                &quot;char_level&quot;: char_level,
            &#125;,
        )</code></pre></details>
<h3 id="fastvideo.eval.metrics.audio.wer.metric.WERMetric.to"><code>fastvideo.eval.metrics.audio.wer.metric.WERMetric.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)
    return self</code></pre></details>
<h3 id="fastvideo.eval.metrics.audio.wer.metric.WERMetric.setup"><code>fastvideo.eval.metrics.audio.wer.metric.WERMetric.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
    if self._asr_backend == &quot;whisper&quot;:
        self._setup_whisper()
    elif self._asr_backend == &quot;glm_asr&quot;:
        self._setup_glm_asr()
    else:
        self._setup_sensevoice()</code></pre></details>
<h3 id="fastvideo.eval.metrics.audio.wer.metric.WERMetric.compute"><code>fastvideo.eval.metrics.audio.wer.metric.WERMetric.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()

    audio = sample.get(&quot;audio&quot;)
    if audio is None:
        return self._skip(sample, &quot;missing &#x27;audio&#x27;&quot;)
    text = sample.get(&quot;reference_text&quot;, sample.get(&quot;text_prompt&quot;))
    if text is None:
        return self._skip(sample, &quot;missing reference_text/text_prompt&quot;)
    language = sample.get(&quot;language&quot;)

    import jiwer

    asr = self._transcribe(audio)
    if set(asr).issubset(PUNCTUATION_SET):
        asr = &quot;&quot;

    force_char_level = _resolve_char_level(language)
    ref_for_wer, hyp_for_wer, char_level = _prepare_for_wer(text, asr, force_char_level)
    wer = jiwer.wer(ref_for_wer, hyp_for_wer)

    return MetricResult(
        name=self.name,
        score=float(wer),
        details=&#123;
            &quot;transcription&quot;: _normalize_text(asr),
            &quot;reference_text&quot;: _normalize_text(text),
            &quot;asr_backend&quot;: self._asr_backend,
            &quot;char_level&quot;: char_level,
        &#125;,
    )</code></pre></details>
</div>
