Install runtime and development dependencies from the repository root:
python -m pip install -r requirements-dev.txtRun the GUI or CLI directly:
python main.py
python main.py --cli --helplaunch.sh and launch.bat provide convenience launchers. On Windows,
launch.bat prefers the active Conda environment when available.
Smart mode requires an FFmpeg build whose libvmaf, siti, and scdet
filters can run. An explicitly supplied FFmpeg/FFprobe pair always takes
priority and is never replaced by a system fallback. Otherwise QualityBound
checks the repository FFmpeg/ directory before the system path.
Supported bundled layouts are FFmpeg/ffmpeg plus FFmpeg/ffprobe, or the same
pair under FFmpeg/bin/, with .exe suffixes on Windows.
Built-in language packs live in config/i18n/. English is the complete
baseline and every built-in pack covers the same keys. User language packs are
partial JSON overrides placed in the writable translations/ directory.
In a source checkout that directory is <repo>/translations/. In the macOS app
it is ~/Library/Application Support/QualityBound/translations/. A language
pack must provide a string language.name; invalid entries are skipped with a
startup diagnostic and missing entries fall back to English.
The QualityBound product name is not translated. Locale packs use QualityBound
for app.title.
ruff check .
pyright
python -m unittest discover -s test -p "test_*.py" -v
python scripts/build_icons.py --checkArchitecture checks can be run separately:
python -m unittest discover -s test -p "test_architecture.py" -vGUI intake discovers and probes sources into AWAITING_START records containing
source drafts, with no execution plan or tool snapshot. Directory recursion is
resolved during intake; Start does not rescan the folder. QueuePrepareWorker
freezes current options, output directory, tools and workdir at Start, preferring
each source's independent parameter and directory overrides. Metadata is probed
again using the start-time tools to account for source changes. Parameter edits
and output naming/validation do not run during intake or right-click override
selection.
Preparation locks edits, reordering and decision activation. The model commits
the complete prepared batch after validating identities and output collisions;
Stop or close before handoff leaves drafts unbound and starts no encodes. Global
tool/encoder or queue collision errors leave the batch awaiting correction;
per-file probe/output validation errors become Failed records, while valid items
run. A pause or Smart decision resumes the bound plan. Manual retry returns to
the draft and preserves its overrides, selecting current global settings at the
next Start. The obsolete GUI PlanWorker and eager planning callback are removed.
When resumed and new items carry different tool snapshots, contiguous groups use their own FFmpeg/FFprobe/workdir. All groups finish analysis before encoding; groups encode in queue order with at most the configured concurrency. The core executors accept matching analysis terminal results and do not measure a group again during the encode phase. Ready Smart items still require successful analysis and the usual final validation/publication gates.
Both Smart algorithms use the same analysis-policy resolver. A size conflict
first applies the selected size or quality relaxation to measured candidates.
If it remains size-blocked with a required output ratio greater than 1.0, it
becomes an intentional SMART_PREDICTED_OVERSIZE skip. An unknown ratio or a
ratio equal to 1.0 remains a decision when no configured relaxation succeeds.
An unreachable quality target follows the separate Skip / Ask policy; a failed
holdout quality check belongs to this outcome, while measurement and tool errors
remain failures. Actual full-encode size misses retain their preserved-file
decision lifecycle and never use the prediction skip rule.
The queue emits an execution-stopped handoff even if some items need decisions. MainWindow asks the size-conflict and quality-miss questions, then the completion handler applies Copy / Ask / Ignore to intentional skips. Cancelling an analysis question leaves that item pending without blocking other source copies. The same analysis result is not automatically asked again during that run; reanalysis can produce a new question. A source-copy refusal records an ignored outcome.
Copies operate on per-item snapshots in a background worker, through an adjacent temporary file and atomic publication. Exclusive publication protects an output that appears during copying when overwrite is disabled. Stop cancels the current copy and removes its temporary file; completed outcomes survive a resumed run. Copy failures update the item to Failed with a diagnostic. The GUI stays busy through this handoff, and only schedules newly executable items after it finishes. Final reports and post-run actions wait until decisions are resolved.
Regression coverage includes fresh default configuration without opening Settings, mixed pending/skipped batches, the strict greater-than-source threshold in both algorithms, holdout policy routing, repeated completion, cancellation and output publication races. These synthetic tests verify control flow and file side effects, not real-world VMAF accuracy.
Measured as a warning over the application scope (core, cli, gui, main.py)
the rule reports 19 findings across 14 files, and every one is an intentional
multi-line string, not an omitted comma:
- a sentence split to stay readable, such as
cli/cli_entry.py:145("...--backend auto. " "Choose a concrete backend..."); - an f-string continued on the next line inside a call, such as
core/encoding/analysis.py:149; - the two halves of a filtergraph, such as
core/smart/v1/vmaf.py:301; _write_analysis_header(core/smart/v1/workflow.py:128), a 15-literal block ofkey=value\nlines, plus the two smaller log blocks it resembles.
Adopting the rule means rewriting each site with + or replacing the log blocks
with str.join, so a readable message or log header would be made less readable
to guard against a defect class the audit found zero instances of. ruff already
declines the style families for the same reason, and this repository wants the
bug-finding subset instead (pyproject.toml).
Re-measure by temporarily adding "reportImplicitStringConcatenation": "warning"
to the project include scope. If a future finding is a genuine concatenation
defect, that site is worth fixing whether or not the rule is adopted.
Four known gaps are left open on purpose. None of them blocks a release. Each is
recorded with the measurement that produced it, so it can be picked up without
re-running the audit. The scope of each check — strict for the application, basic
for test/ and scripts/ — is recorded with its reason in the same section.
pyproject.toml turns off six strict rules: reportMissingTypeArgument,
reportUnknownArgumentType, reportUnknownLambdaType, reportUnknownMemberType,
reportUnknownParameterType, reportUnknownVariableType. Measured over the current
scope (core, cli, gui, main.py) they hold back 610 diagnostics: 483 in
core, 116 in gui, 11 in cli. The dominant pattern is capability snapshots,
preset documents and analysis receipts travelling as dict[str, object] or
JSON-decoded mappings, so the checker cannot name the types that flow through them.
That number is ratcheted rather than merely written down. pyright.strict-debt.json
extends the project config and raises those six rules to warnings;
scripts/check_strict_debt.py measures through it and fails the Quality job when
the total or any package count grows above the baseline recorded in that script.
Shrinking is always allowed, and the new numbers are recorded in both places:
python scripts/check_strict_debt.pyClosing it means naming those boundaries inside core — a TypedDict or dataclass
per receipt kind, capability snapshot and preset document — and letting the adapters
build them. A restructuring of that size needs its own diff and independent review,
so it was not folded into the strict-mode adoption.
The annotations added for the strict adoption (gui/qt_optionals.maybe_none,
QueueTableModel._transient_index, core.smart.v1.bitrate._SharedSearchResultFields,
the __all__ lists in core/encoding) are not suppressions. They stay checked by
the remaining strict rules, and the __all__ lists keep working after the six rules
come back on.
pyright.tests.json includes test/ and runs the project config in basic mode
with reportUnnecessaryTypeIgnoreComment, so an unnecessary type: ignore in a
test is an error rather than an unchecked comment. The Quality job runs it after
the strict pass over the application.
strict is not available for test/ from the project config: pyright resolves
typeCheckingMode per configuration file, not per directory, so a stricter pass
would apply the same rules to both scopes. A strict pass over test/ reports
findings in three classes, and the first two are intrinsic to testing rather than
fixable defects:
reportPrivateUsage— tests deliberately call the module-private helpers they cover (_run_command,_validate_decode_acceleration,_imports);reportMissingParameterType— fixture helpers and fake callbacks annotate nothing when the value is a throwaway;reportArgumentTypeandreportOptionalMemberAccesson thedict[str, object]receipt and event payloads, the same boundary the application's strict pass leaves to the disabled inference rules.
Re-measure with pyright -p pyright.tests.json after temporarily raising
typeCheckingMode in that file to strict. Closing the gap means either a
per-directory rule policy (not supported today) or splitting the test tree across
two configuration files, so the decision is recorded here rather than taken
silently.
test/test_prepare_ffmpeg.py holds the only integration test that invokes a real
FFmpeg (VMAF_INTEGRATION_FFMPEG), and it skips when that variable is unset. No
workflow sets it: _package.yml passes --require-ffmpeg to the Nuitka builder,
which selects and verifies a bundled FFmpeg pair rather than measuring with it.
Every other test that touches FFmpeg mocks the subprocess, so the argument
vectors are checked and the emitted filtergraphs are not executed.
The consequence is visible in this file: the Windows behaviour of the Scout
metadata path is unmeasured (see "A filter option value cannot carry a colon or
a quote" under Known limitations), and the segmented-VMAF absolute-path defect
was found by a unit test decoding a command rather than by running one. Closing
it means pointing VMAF_INTEGRATION_FFMPEG at the bundled pair in the native
test job, which also needs a small generated fixture so the test stays fast.
pyright.scripts.json includes scripts/ and runs the project settings in basic
mode. It reports nothing now; the findings it started with were real typing
mistakes rather than style:
scripts/build_icons.pypassed astrtoQImage.save(device, format), which takesbytes, and returnedQByteArraywherebyteswas declared — nowb"PNG"andbytes(encoded.data());scripts/prepare_ffmpeg.pytyped_copy_streamasBinaryIOwhilezipfileandtarfilehand itIO[bytes], and iterateddata["licenses"]without narrowing it out ofobject;scripts/run_smart_case.pypassed the possibly-unsetground_truth_passedandfull_encode_output_bytesintoOraclePoint, which declares them non-optional;scripts/build_nuitka.pybuiltPath(nuitka.__file__)without allowing forNone. The narrowed form matters because the check's result depended on whether Nuitka happened to be installed: an unresolved import falls back totypes.ModuleType, whose__file__isstr | None, so a machine with Nuitka present passed and one without it reported aStrPathargument error.
The Quality job installs requirements-build.txt before this step, so the
nuitka import resolves instead of reporting an unresolved import, and the same
step keeps working for a contributor who has not installed the build
requirements. Strict mode is not available for this scope: a strict pass over
scripts/ reports the same six disabled inference rules the application scope
leaves open (capability snapshots, preset documents and JSON-decoded manifests
travelling as object). These are development and packaging tools, not
application payload, so the scope choice is a cost decision rather than a safety
one; the executables still run in the quality gate (build_icons.py --check), the
packaging workflow (prepare_ffmpeg.py, build_nuitka.py), and
test/test_smart_case.py / test/test_smart_corpus.py.
core/ffmpeg/commands.py:build_subtitle_args maps the source's subtitle
streams into MP4 with -map 0:s? -c:s mov_text. mov_text is a text codec, so
it only accepts text subtitle sources (SubRip, ASS, WebVTT, ...). A source whose
subtitle stream is a bitmap format — PGS (hdmv_pgs_subtitle), DVD/VobSub
(dvd_subtitle), DVB (dvbsub), DVB teletext or XSUB — is rejected by FFmpeg
before the output file is opened:
[sost#0:1/mov_text @ ...] Subtitle encoding currently only possible from text to text or bitmap to bitmap
Error opening output file out.mp4.
Error opening output files: Invalid argument
The behaviour was reproduced with a one-display-set PGS stream, and the text
path (.srt to MP4 mov_text) encodes successfully.
Planning now refuses such a source before any encode runs, so the failure is not
left to the end of the video encode (SPEC.md N9 ← S3: dropping the stream would
publish a file that lost part of the input, and switching the container would
change the requested output). core/media/subtitles.py names the bitmap codecs
in MP4_INCAPABLE_SUBTITLE_CODECS; core/media/validation.py: validate_subtitle_carrier applies them to MediaInfo.subtitle_codecs, which
core/ffmpeg/probe.py fills from the ffprobe stream list. The item becomes a
skipped plan item whose reason names the codec, with the source unchanged.
MKV output is unaffected: it copies the stream with -c:s copy, so bitmap
subtitles survive as they are. Users who need bitmap subtitles select MKV;
disabling subtitle copying also plans for MP4, and that choice is the
operator's, not a silent drop.
core/ffmpeg/filters.py owns the escaping rule for values interpolated into a
filtergraph. Escaping the backslash works; escaping the colon and the quote does
not. Measured with FFmpeg 9.0.2 on macOS arm64, one invocation per case, using
-vf 'metadata=mode=print:file=<value>' and listing the directory afterwards:
value passed to file= |
exit | file written |
|---|---|---|
out.txt |
0 | out.txt |
c\d.txt (raw) |
0 | cd.txt — the backslash is consumed |
c\\d.txt (escaped) |
0 | c\d.txt |
a:b.txt (raw) |
234 | none — Invalid argument |
a\:b.txt (escaped) |
8 | none — Protocol not found |
e'f.txt (raw) |
0 | ef.txt — the quote is consumed |
e\'f.txt (escaped) |
0 | ef.txt |
So only the backslash replacement makes a value survive. The colon is read as a
protocol separator and the quote as filtergraph quoting before the value ever
reaches the filter, and escaping moves the error without fixing the path. The
replacement chain stays in core/ffmpeg/filters.py because the emitted value is
still parsed as a filtergraph, and no caller may hand it a value needing either
rule.
That is why every libvmaf log_path call site now passes a bare file name
(json_path.name, log_path.name) while running FFmpeg with cwd set to the
directory holding it. scripts/run_smart_case.py passed an absolute path until
that change; on Windows it embedded a drive path (D:\a\...), and the Windows
test job failed decoding it. The Scout metadata file
(core/smart/v1/sampling/scout.py) still embeds an absolute temp_root path,
which on Windows contains a drive colon. Not verified: no CI job runs Smart
analysis against a real FFmpeg — test/test_prepare_ffmpeg.py skips the only
integration test unless VMAF_INTEGRATION_FFMPEG is set, and no workflow sets
it — so the Windows behaviour of that path has never been observed. Verifying it
means running one real Scout on Windows with VMAF_INTEGRATION_FFMPEG pointing
at the bundled FFmpeg.
Install build requirements and create a native package on the current platform:
python -m pip install -r requirements-build.txt
python scripts/build_nuitka.py --clean --version 3.4.0The normalized standalone directory is dist/qualitybound/. A native macOS app
build produces dist/QualityBound.app/ and dist/qualitybound.dmg.
Tagged releases prepare and require the pinned FFmpeg pair before packaging. The release matrix contains Windows x86-64/ARM64, Linux x86-64/ARM64, and macOS arm64, producing eight platform packages. Read Release map before changing build, packaging, or workflow behavior.