Skip to content

Commit 3a48194

Browse files
hclivessclaude
andcommitted
The target belongs to the metric, not to the search
A quality search on a machine without libvmaf recommended the bottom of the CRF range on every file and reported the target as never reached. The settings said VMAF >= 95. The FFmpeg in use had no libvmaf, so the search quietly measured with SSIM instead — and kept the 95. SSIM counts to 1.0. Every probe read below target, the bisection walked to the bottom of the range and recommended it, file after file, for a reason nothing on screen explained. The ordinary Windows 'essentials' FFmpeg build has no libvmaf, so this was the common case, not the exotic one. A number outside a metric's own range is no longer treated as a target: the metric's default is used instead, in the search, in the Quality tab and in loaded presets. The tab no longer selects a metric this build cannot compute and says why when it substitutes; a search that has to substitute reports it in its progress and in its result. And a search that reaches nothing now names what it did reach, rather than leaving the unreachable number unstated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdAC2Zh8ccUhNm5zyQjsCw
1 parent a798326 commit 3a48194

5 files changed

Lines changed: 87 additions & 11 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ and a grainy film print never wanted the same one.
1515

1616
![videer processing a queue](thumb.png)
1717

18+
## Changes in 3.13.4
19+
20+
- **The quality search recommended the bottom of the CRF range on every file, and said the target was never
21+
reached.** A target belongs to the metric it was set for: VMAF counts to 100, SSIM to 1.0, XPSNR in
22+
decibels. When the FFmpeg in use has no `libvmaf` — the ordinary Windows "essentials" build has none —
23+
videer quietly measured with SSIM instead but kept the number 95 that had been set for VMAF. SSIM cannot
24+
reach 95; nothing can. So every probe read *below target*, the search walked to the bottom of the range and
25+
recommended it, on file after file, for a reason nothing on screen explained.
26+
- A target from another metric's scale is now refused outright and that metric's own default used instead —
27+
in the search, in the Quality tab and in loaded presets alike. The Quality tab no longer selects a metric
28+
this build cannot compute, and says why when it has to choose another one. A search that has to substitute
29+
says so in its progress and in its result.
30+
- **"No CRF reached the target" now says what *was* reachable** — "the best anything in that range managed was
31+
SSIM 0.9721 at CRF 16, so a target above that is out of reach for this source" — instead of leaving the
32+
number that could not be met unstated.
33+
34+
For VMAF specifically: it needs an FFmpeg built with `libvmaf`. On Windows the gyan.dev *full* builds and the
35+
BtbN builds have it; the *essentials* build does not.
36+
37+
1838
## Changes in 3.13.3
1939

2040
- **A subtitle track no longer takes the whole file down with it.** Encoding a subtitled MP4 to MKV failed

config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
# Application info
1111
APP_NAME = "videer"
12-
APP_VERSION = "3.13.3"
12+
APP_VERSION = "3.13.4"
1313
WINDOW_MIN_WIDTH = 1200
1414
WINDOW_MIN_HEIGHT = 900
1515

modules/preset_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ def apply_settings(self, settings: Dict[str, Any]):
238238
if 'quality_metric' in settings:
239239
ui._select_quality_metric(settings['quality_metric'])
240240
if 'quality_target' in settings:
241-
ui.controls['quality_target'].setValue(float(settings['quality_target']))
241+
# After the metric, and through the guard: a target saved for VMAF is not a target for SSIM
242+
ui.set_quality_target(settings['quality_target'])
242243
if 'quality_pool' in settings:
243244
index = ui.controls['quality_pool'].findData(settings['quality_pool'])
244245
if index >= 0:

modules/quality_analyzer.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,33 @@ def choose_metric(preferred: str = DEFAULT_QUALITY_METRIC) -> str:
124124
return DEFAULT_QUALITY_METRIC
125125

126126

127+
def metric_target(metric: str, value: Any = None) -> float:
128+
"""
129+
A target that means something on this metric's scale.
130+
131+
VMAF counts to 100, SSIM to 1.0, XPSNR in decibels. A number carried over from another metric is not a
132+
demanding target, it is an impossible one: 95 asked of SSIM can never be met, so every probe reads "below
133+
target", the search walks to the bottom of the CRF range and recommends it — on every file, for a reason
134+
nothing on screen explains. Anything outside the metric's own range is therefore not a target at all, and
135+
the metric's default is used instead.
136+
"""
137+
spec = metric_spec(metric)
138+
try:
139+
number = float(value)
140+
except (TypeError, ValueError):
141+
return float(spec['default_target'])
142+
low, high = spec['range']
143+
return number if low <= number <= high else float(spec['default_target'])
144+
145+
146+
def substitution_note(requested: str, used: str, target: float) -> str:
147+
"""Why the search is not measuring what it was asked to measure"""
148+
asked, actual = metric_spec(requested), metric_spec(used)
149+
return (f"This FFmpeg has no {asked['filter']} filter, so {asked['label']} cannot be measured — "
150+
f"searching on {actual['label']} \u2265 {target:g}{actual['unit']} instead. VMAF and MS-SSIM need "
151+
f"an FFmpeg built with libvmaf; SSIM, PSNR and XPSNR are in every build.")
152+
153+
127154
def format_score(metric: str, value: Optional[float]) -> str:
128155
"""'VMAF 95.2' / 'PSNR 42.10 dB' — the number with enough context to be read on its own"""
129156
if value is None:
@@ -299,9 +326,12 @@ def __init__(self, filepath: str, settings: Dict[str, Any],
299326
self._should_stop = should_stop or (lambda: False)
300327
self._on_process = on_process
301328

302-
self.metric = choose_metric(self.settings.get('quality_metric', DEFAULT_QUALITY_METRIC))
303-
self.target = float(self.settings.get('quality_target')
304-
or metric_spec(self.metric)['default_target'])
329+
requested = self.settings.get('quality_metric', DEFAULT_QUALITY_METRIC)
330+
self.metric = choose_metric(requested)
331+
# A target set for a metric this build cannot compute belongs to that metric, not to the stand-in
332+
self.substituted_for = requested if (requested != self.metric and requested in QUALITY_METRICS) else None
333+
self.target = metric_target(self.metric,
334+
None if self.substituted_for else self.settings.get('quality_target'))
305335
self.pool = self.settings.get('quality_pool', DEFAULT_QUALITY_POOL)
306336
self.sample_count = int(self.settings.get('quality_samples') or QUALITY_SAMPLE_COUNT)
307337
self.sample_seconds = float(self.settings.get('quality_sample_seconds') or QUALITY_SAMPLE_SECONDS)
@@ -377,6 +407,8 @@ def _search(self) -> Optional[Dict[str, Any]]:
377407
f"{metric_spec(self.metric)['label']}{self.target:g} "
378408
f"({pool_label(self.pool).lower()}), CRF {self.crf_low}{self.crf_high} "
379409
f"— about {probes_expected} probes.")
410+
if self.substituted_for:
411+
self._progress(substitution_note(self.substituted_for, self.metric, self.target))
380412

381413
self._workdir = tempfile.mkdtemp(prefix=WORKDIR_PREFIX)
382414

@@ -542,15 +574,21 @@ def _summarize(self, best: Optional[Dict[str, Any]], results: Dict[int, Dict[str
542574
"""Turn the probes into a recommendation plus the caveats that go with it"""
543575
source_size = info.get('size')
544576
notes: List[str] = []
577+
if self.substituted_for:
578+
notes.append(substitution_note(self.substituted_for, self.metric, self.target))
545579

546580
if best is None:
547581
# Nothing in the range held the target. The closest attempt is the honest recommendation, and the
548582
# reason is almost always grain or noise: detail that costs a great many bits to reproduce.
549583
closest = max(results.values(), key=lambda p: p['score']) if results else None
584+
best_line = (f" The best anything in that range managed was "
585+
f"{format_score(self.metric, closest['score'])} at CRF {closest['crf']}, so a target "
586+
f"above that is out of reach for this source." if closest else "")
550587
notes.append(
551-
f"No CRF in {self.crf_low}{self.crf_high} reached the target. The source is probably grainy, "
552-
f"noisy or already heavily compressed — quality that expensive to keep is a sign that "
553-
f"re-encoding it will not save much.")
588+
f"No CRF in {self.crf_low}{self.crf_high} reached "
589+
f"{format_score(self.metric, self.target)}.{best_line} A grainy, noisy or already heavily "
590+
f"compressed source is the usual reason — detail that expensive to keep is also a sign "
591+
f"that re-encoding it will not save much.")
554592
recommended = closest
555593
else:
556594
recommended = best
@@ -847,8 +885,7 @@ def _load_from_settings(self, settings: Dict[str, Any]):
847885
"""Start from the Quality tab, so the dialog and the batch matcher agree until told otherwise"""
848886
index = self.metric_combo.findData(self.metric_key)
849887
self.metric_combo.setCurrentIndex(index if index >= 0 else 0)
850-
self._configure_target(float(settings.get('quality_target')
851-
or metric_spec(self.metric_key)['default_target']))
888+
self._configure_target(metric_target(self.metric_key, settings.get('quality_target')))
852889

853890
pool_index = self.pool_combo.findData(settings.get('quality_pool', DEFAULT_QUALITY_POOL))
854891
self.pool_combo.setCurrentIndex(pool_index if pool_index >= 0 else 0)

modules/ui_manager.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
DEFAULT_QUALITY_POOL)
2525
from modules.process_manager import format_duration, format_size
2626
from modules.quality_analyzer import (QualityMatchDialog, choose_metric, fill_metric_combo,
27-
format_score, metric_spec, resolved_search_range)
27+
metric_target, format_score, metric_spec,
28+
resolved_search_range)
29+
from config import QUALITY_METRICS
2830
from modules.repair_manager import RepairDialog
2931

3032

@@ -908,6 +910,10 @@ def _select_quality_metric(self, metric: str):
908910
well would re-run it with no remembered value and overwrite the target the user last set for this
909911
metric with the factory one.
910912
"""
913+
# A metric this build cannot compute is in the list but greyed out; selecting it anyway would leave
914+
# the tab showing a target that nothing measures against. Settle for one that can be measured.
915+
requested = metric
916+
metric = choose_metric(metric)
911917
combo = self.controls['quality_metric']
912918
index = combo.findData(metric)
913919
if index < 0:
@@ -919,6 +925,18 @@ def _select_quality_metric(self, metric: str):
919925
self._current_quality_metric = metric
920926
self._configure_quality_target(metric, self._quality_targets_seen.get(metric))
921927

928+
if metric != requested and requested in QUALITY_METRICS:
929+
# Say it where the choice is made, rather than leaving a greyed-out entry to explain itself
930+
self.quality_metric_note.setText(
931+
f"{metric_spec(requested)['label']} needs an FFmpeg built with "
932+
f"{metric_spec(requested)['filter']}, and this one has no such filter — "
933+
f"{metric_spec(metric)['label']} selected instead.")
934+
935+
def set_quality_target(self, value: Any):
936+
"""Set the target the Quality tab shows, but only if it is a score the current metric can produce"""
937+
metric = self.controls['quality_metric'].currentData() or DEFAULT_QUALITY_METRIC
938+
self.controls['quality_target'].setValue(metric_target(metric, value))
939+
922940
def _configure_quality_target(self, metric: str, value: Optional[float] = None):
923941
"""0-100, 0-1 and decibels share no numbers: the target control is rebuilt per metric"""
924942
spec = metric_spec(metric)

0 commit comments

Comments
 (0)