Skip to content
blast1seePublic

About

A comparison workbench for video and audio releases: frame-matched screenshots, player-settings comparisons, audio analysis, and publishing to slow.pics or comp.pics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

kiyas

A comparison workbench for video and audio releases: generate frame-matched screenshots from several sources, analyse audio, and publish the result to slow.pics or comp.pics with forum-ready BBCode.

kıyas is Turkish for "comparison".

Status: usable. Point it at two or more files and it writes frame-matched, tonemapped screenshots and publishes them. Point it at one file and a list of player settings and it renders that frame every way you asked for. Point it at audio tracks and it measures them. There is a window if you want one. See Roadmap.


Why another one

Two workflows that look different turn out to be the same problem:

Source comparison — several releases of the same title, one frame, side by side. Normally done by editing a VapourSynth script by hand and opening a previewer.

Settings comparison — one source, one frame, rendered several different ways: tone-mapping curves (spline vs bt.2446a vs st2094-40), upscaling shaders, scalers, deband strengths. This is how you decide what your player config should actually be, and it cannot be done in VapourSynth at all — GLSL shaders and libplacebo curves only exist inside mpv.

Both need the same three things: produce the frames, label them, share them. kiyas does those three things for both.

Design constraints

Nothing is installed system-wide. Since VapourSynth R74 the entire stack is pip-installable, and every plugin wheel drops its binary into <site-packages>/vapoursynth/plugins. kiyas builds one virtualenv and puts everything inside it. Deleting .venv undoes the installation completely.

The one file written outside the project is %APPDATA%/vapoursynth/vapoursynth.toml, which vapoursynth config needs so VSScript can find the interpreter. It is additive and keyed by this environment's own vsscript path, so other VapourSynth installations are untouched. kiyas never runs vapoursynth register-install, register-legacy-install or register-vfw — those write to the registry and exist for people who want one blessed system-wide install, which is the opposite of what this tool is for. A test asserts those strings do not appear in the source.

Your mpv config is never touched. mpv is always invoked with --config-dir pointing at a directory kiyas owns. When that flag is set mpv reads only that directory; %APPDATA%/mpv is not read, not merged and not written. Project files cannot set the options that would undo this. Shader files you name are read where they live, and never copied or modified.

The GUI has no privileges. It builds a project, writes it as TOML, and calls the same core the CLI calls. Anything the window can do is reachable from the command line, the project file is shareable and diffable, and the core stays testable without a display. It also validates by writing the project out and reading it back through the ordinary parser, so it cannot accept something kiyas run would refuse -- and the message it shows you is that parser's.

Binaries resolve from absolute PATH entries only. kiyas usually runs with the working directory set to wherever the media lives, so an ffmpeg.exe dropped next to a release must never be executed.

Requirements

  • Windows, Linux or macOS
  • Python 3.12+ (3.13 recommended)
  • FFmpeg on PATH — a full build, for libplacebo, zscale and showspectrumpic
  • mpv 0.36+ on PATH, built with libplacebo — for settings comparisons and the frame picker. Not needed to compare files against each other. If it lives somewhere unusual, name it under [tools] in the project file instead of putting it on PATH
  • Optional: MediaInfo for detailed audio metadata, dovi_tool for composing Dolby Vision profile 7 enhancement layers

Install

A packaged build, from releases: unzip it and run kiyas-gui.exe for the window or kiyas.exe for the command line. No Python needed. It has the ffmpeg and mpv engines — VapourSynth is a stack of compiled plugins that installs into a Python environment, so that one needs a checkout. Each release carries a .sha256 beside the zip, written by the same runner that built it from the tagged commit.

Audio comparisons need a checkout too. numpy, scipy and matplotlib come to 180 MB against a 58 MB package, and the whole argument for the packaged build is that it is the small download. Picture comparisons and publishing work there; kiyas audio says so rather than telling you to run a pip install it has no pip for.

A checkout, for the VapourSynth engine and for working on it:

git clone https://github.com/blast1see/kiyas
cd kiyas
.\bootstrap.ps1

That creates .venv, installs kiyas, installs the VapourSynth stack into the virtualenv and runs the environment check. To skip the ~400 MB VapourSynth download and use the ffmpeg engine instead:

.\bootstrap.ps1 -SkipVapourSynth

On Linux and macOS, or if you prefer to do it by hand:

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,audio]"
kiyas setup      # installs the VapourSynth stack into this venv
kiyas doctor     # reports what is available

Optional extras:

pip install -e ".[gui]"                                       # desktop interface
pip install git+https://github.com/blast1see/AudioSyncTool    # accurate offset measurement

Usage

kiyas doctor              # what engines and tools are available, and what is missing
kiyas setup               # install the VapourSynth stack into the current environment
kiyas init project.toml   # write a starter project file
kiyas run project.toml    # produce the comparison
kiyas publish out/        # upload it to a comparison host
kiyas align project.toml  # measure how far apart the sources are, in frames
kiyas pick film.mkv       # choose frames by watching, in mpv
kiyas templates           # list the built-in settings comparisons
kiyas audio a.mkv b.mkv   # compare audio tracks
kiyas gui                 # the same things, in a window

A project is a TOML file. Six sources with individual crops, trims and tonemapping do not fit on a command line, and the description is worth keeping: it is what you edit when the sync turns out to be a frame off, and what you hand to someone else so they can reproduce the comparison exactly.

title = "1917 (2019)"
engine = "auto"           # auto | vapoursynth | ffmpeg

[frames]
method = "count"          # count | interval | manual
count = 12
skip_start = "5%"         # skip logos and black leader
skip_end = "10%"          # skip credits
b_frames_only = true      # I-frames get more bitrate and flatter a weak encode
skip_dark = true          # a black frame compares nothing
# dark = 2                # ...but ask for the darkest usable frames as well
# light = 2               # ...and the brightest
# skip_combed = true      # for an interlaced-source release; see below

[[source]]
path = "1917.2160p.UHD.BluRay.REMUX.DV.HDR.mkv"
name = "UHD REMUX"
tonemap = "auto"          # auto | hdr10 | hdr10plus | dovi | none
dovi_el = "auto"          # off | auto | on -- Dolby Vision enhancement layer

[[source]]
path = "1917.1080p.BluRay.x264.mkv"
name = "1080p BluRay"
trim = 24                 # frames to drop from the start, to line it up
# crop = [0, 0, 140, 140] # left, right, top, bottom
# resize = [1920, 1080]

[output]
directory = "out"

Output is one directory per source, each holding PNGs named after the frame number, plus kiyas-manifest.json recording exactly what was produced.

Two rules are on by default and worth knowing about. B-frames only: I-frames land in different places in every encode and get a disproportionate share of the bitrate, so comparing them flatters the weaker encode — kiyas nudges each selected position forward until the frame is a B-frame in every source. Sources have to be the same size. Two columns of different shapes cannot be flipped between, and flipping between them is the one thing a comparison is for. A 2.40:1 WEB-DL is coded at its picture size while the disc keeps the bars inside a 16:9 frame, so the disc needs a crop to match. kiyas says so when they come out different rather than publishing two different shapes.

It does not suggest the numbers, because the obvious arithmetic is wrong: splitting the difference on a real pair gives 277 rows top and bottom where the source's own Dolby Vision metadata says 276, and the remaining row comes from the other release's conformance window. On a DV source the active picture is in the RPU's level 5 offsets:

ffmpeg -v error -i film.mkv -map 0:v:0 -c:v copy -bsf:v hevc_mp4toannexb -f hevc - |
  dovi_tool extract-rpu - -o rpu.bin
dovi_tool info -i rpu.bin -f 100      # active_area_top_offset / _bottom_offset

Skip dark: a frame that is essentially black compares nothing. Brightness is measured over the centre of the picture so letterbox bars do not skew it.

Combed frames are off by default and worth understanding before turning on. A frame with interlacing combs compares the deinterlacer's work rather than the encode's, so on a release cut from an interlaced master skip_combed = true is the rule you want. It needs the VapourSynth engine -- ffmpeg's idet answers for a stream, not for one frame -- and the orchestrator says so and carries on rather than pretending the rule is active.

It is off by default because the detector reads high-frequency horizontal detail as combing. Measured on a real film clip and an interlaced copy of itself: 19 of 30 combed frames caught and none of the progressive original flagged. Measured on ffmpeg's testsrc2 and mandelbrot, whose hard edges are nothing like film: 20 progressive frames out of 20 flagged. Animation has hard edges too, so turn this on for the material it is for and check what it kept.

dark and light add frames on top of the evenly spaced ones, chosen for being the darkest and brightest of a sample. Even spacing finds the typical frame, and the two questions people actually bring to a comparison do not live there: banding and block noise are in the dark scenes, highlight rolloff and specular detail are in the bright ones. skip_dark still applies as a floor -- the darkest frame of most films is a fade to black, which compares nothing at all. Measured on a 4K WEB-DL asking for four evenly spaced frames plus two of each: the picks came out at 0.10 and 0.10 against an evenly spaced range of 0.18 to 0.32, and at 0.37 and 0.63 above it.

Dolby Vision profile 7

A profile 7 release carries its picture in two layers: a base layer any HDR10 decoder can read, and an enhancement layer only a Dolby Vision decoder composes back in. Screenshots of the base layer alone are not what a Dolby Vision player shows, and comparing one release's base layer against another's compares something neither viewer sees.

[[source]]
path = "film.2160p.UHD.BluRay.REMUX.DV.HDR.mkv"
name = "UHD Remux"
dovi_el = "on"            # compose the enhancement layer in

auto, the default, detects the layer and says so in the run's warnings without composing it. That is deliberate: extracting the layer reads the whole file, and doing that unasked is a worse surprise than a stated caveat. Measured on a 78 GB profile 7 remux -- 10.3 minutes, and a 4.64 GB layer, which is kept and reused. off is silent.

on needs dovi_tool and the VapourSynth engine, and fails rather than warning if it cannot do what it was asked. The extracted layer goes wherever [output] index_dir points, or beside the media, for the same reason indexer caches do.

resize cannot be combined with it: the enhancement layer is composed pixel against pixel with the base layer, and resampling either destroys that.

Checking the sync

trim lines two releases up, and until you check it there is nothing in the output to say whether it is right -- a wrong trim is wrong in every frame and every frame still looks like a frame.

kiyas align project.toml         # measure it
kiyas run project.toml --check-sync

It reports how far each source is from the first, how confident that is, and the trim lines to paste in. Positive means the source plays later, which is the number to add to its trim. A weak match says so rather than handing you a number that looks like an answer.

This is not part of an ordinary run. run decodes a few dozen frames; this decodes thousands, and making every comparison slower to catch a mistake most projects do not have is the wrong trade.

Settings comparisons

The other axis: one file, one frame, rendered several different ways. This is how you decide what your player config should be, and mpv is the only engine that can do it — tone-mapping curves and GLSL shaders live inside a renderer.

kiyas init tonemap.toml --settings
kiyas templates            # what the built-in sets contain
kiyas run tonemap.toml
title = "1917 — tone mapping"
mode = "settings"

[[source]]
path = "1917.2160p.UHD.BluRay.REMUX.DV.HDR.mkv"
name = "1917 UHD"

[frames]
method = "manual"
manual = [36869, 67793]   # from `kiyas pick`

[settings]
template = "tonemap"      # tonemap | gamut | scalers | dscale | deband | shaders
# width = 1920            # capture width; the height follows the source aspect
# fullscreen = true       # capture at the full screen resolution
# base = { target-peak = 203 }   # applied to every variant

# Or spell the variants out. The name becomes the column label.
[[variant]]
name = "ArtCNN C4F32"
options = { glsl-shaders = "~/mpv/shaders/ArtCNN_C4F32.glsl" }

Shader files are read where they are. kiyas never copies, edits or imports anything from your player configuration.

About the capture size. A settings comparison is captured at your display size, not the source's — 2474x1392 on a 2560x1440 screen, or exactly 2560x1440 with fullscreen. A source comparison is not: that one is always the source's own resolution, and no display is involved at any point.

The difference is not an oversight in one of them. What a settings comparison compares only exists on a display:

  • A tone-mapping curve maps HDR to a display. Ask mpv for a source-resolution capture and it writes the frame in its own colourspace, where no mapping has happened — measured, and all four curves come back byte-identical.
  • An upscaling shader runs because the display is larger than the source. ArtCNN carries its own condition to that effect: at source resolution it does not fire, and the capture is byte-identical to having no shader at all.

So "the same comparison at 4K" is not a thing being withheld; for a display transform it is not a question with an answer. Set width explicitly if you want the same size on every machine — the default follows the screen, and the size that came out is reported after every run.

Audio

kiyas audio original.mkv dubbed.mkv
kiyas audio a.mkv b.mkv --track 1        # a different audio stream from each file
kiyas audio a.mkv b.mkv --publish        # upload it like any other comparison

Each track gets a spectrogram, a waveform with clipping marked, and an average frequency response — laid out identically so they can be flipped between — plus a specification table in Markdown and BBCode, and the offset between the tracks.

What it measures rather than reads off the header:

  • Real bit depth. A 24-bit container holding 16-bit content is common and nothing in the header says so. Refused for lossy codecs, where the answer would be a measurement of the decoder rather than the file.
  • Clipping, counted as runs of consecutive full-scale samples. One sample at full scale happens in any loud master; three in a row does not.
  • Silent channels, which is what a fake 5.1 upmix looks like from the inside.
  • Identical channels, which is what a mono dub in a stereo container looks like — and what a frequency-response plot cannot show you, because the two curves lie exactly on top of each other.
  • The offset, because comparing two tracks that are not aligned measures the misalignment and nothing else. +250 ms means the second track plays later. AudioSyncTool is used when it is installed and its drift fit is wanted; otherwise a single GCC-PHAT correlation, with the peak-to-floor ratio reported so a weak match is visible as one.

The window

kiyas gui                     # empty, ready for files dropped onto it
kiyas gui project.toml        # open a project
kiyas gui a.mkv b.mkv         # start from these files
pip install -e ".[gui]"       # if PySide6 is not installed yet

Drop files on it, pick what kind of comparison it is, press Run. It does the same three things the commands do -- source comparisons, settings comparisons, audio -- and saves what you set up as a project file you can run from a terminal, hand to someone else, or keep in a repository.

The window never decides anything the command line cannot. What is on screen becomes a project, the project is written and read back through the same parser, and the sentence under the file list is whatever that parser said.

Choosing frames by hand

Automatic selection spreads captures evenly and avoids black frames, which is the right default and no help at all when the shot that settles the argument is a specific one.

kiyas pick film.mkv

That opens the file in mpv with normal playback controls. Press s to mark the frame you are on, u to undo, q to finish. It prints a [frames] block ready to paste into a project file — it does not edit your project, because guessing whether to add or replace would eventually destroy work.

Publishing

kiyas publish out/                  # unlisted, PNG preserved
kiyas publish out/ --public         # listed on the site
kiyas publish out/ --remove-after 7 # let the host delete it after a week
kiyas run project.toml --publish    # do both in one go

Comparisons are unlisted by default. A comparison is usually a working document, and putting one on the site's front page should be a decision rather than something that happens because you did not pass a flag.

Every image is hashed before upload and the digests go up with the collection metadata, so slow.pics can say which ones it already holds. Re-running a publish that died halfway only sends what is missing.

Naming the title

slow.pics can attach a comparison to a TMDB entry, and --tmdb takes the reference it wants:

kiyas publish out/ --tmdb MOVIE_1275779
kiyas publish out/ --tmdb "The Prestige"     # looked up by name

Nobody knows that number, so anything that is not a reference is searched for instead. That needs a TMDB key -- they are free to any account -- in KIYAS_TMDB_API_KEY, read from the environment for the same reason comp.pics' key is: kiyas has no credential store and is not gaining one for a field that is optional.

A name that matches one title resolves. A name that matches several is a question rather than an answer -- "Dune" is two films twenty years apart -- so the candidates are printed and you pass the one you meant. A bare number is still refused: slow.pics needs to know whether it is a film or a series, and that is the one thing that cannot be guessed.

Choosing a host

kiyas publish out/ --to comppics                 # comp.pics instead
kiyas publish out/ --to comppics --tag remux     # tag it
kiyas publish out/ --to pixhost                  # an image host, see below
kiyas run project.toml --publish --publish-to comppics

slow.pics is the default and nothing about it changed. --to comppics publishes to comp.pics, or to your own instance of the same software:

export KIYAS_COMPPICS_URL=https://comps.example     # or --host-url
export KIYAS_COMPPICS_API_KEY=comps_...             # optional

The key is optional: anonymous uploads work, they simply leave the comparison with no owner, so it cannot be managed from an account afterwards. It is read from the environment rather than a config file because kiyas has no credential store and is not gaining one to hold a token that is not required.

Three things work differently there, and kiyas says so on the way past rather than after the fact:

  • There is no unlisted mode. comp.pics lists every comparison to anyone who asks, so --public has no meaning and publishing there is a more public act than publishing to slow.pics with the same flags. A note is printed before anything is sent.
  • Nothing is kept forever. The expiry is one of 1, 7, 30 or 90 days, so --remove-after is snapped to the nearest of those and the choice is printed. --remove-after 0 — never, on slow.pics — becomes 7 days. --expire-from chooses whether that clock runs from creation or from the last view.
  • --nsfw, --no-optimize and --tmdb have no equivalent and are named as ignored if you pass them.

In exchange, comp.pics gives every image its own URL, which is what the markup formats were written for:

kiyas publish out/ --to comppics --format comparison

On slow.pics that prints a link to the collection, because the upload hands back no per-image addresses. On comp.pics it prints the real thing: one tag holding the whole grid, frame by frame, which is what a reader can flip through.

pixhost, for screenshots rather than comparisons

kiyas publish out/ --to pixhost --format thumbnails
kiyas publish out/ --to pixhost --thumb-size 500

The other two hosts store a comparison: a grid they know the shape of, with a viewer that flips between sources at one frame. pixhost stores pictures. What it gives back for each one is a page and a thumbnail, which is what a forum post wants and what neither of the other two hands over:

[b]0:21:14.083 / 30550[/b]
UHD REMUX: [url=https://pixhost.to/show/...][img]https://t3.pixhost.to/thumbs/...[/img][/url]
1080p BluRay: [url=https://pixhost.to/show/...][img]https://t3.pixhost.to/thumbs/...[/img][/url]

That is the thumbnails format, and it is the one format that does not inline the full picture — which is what makes a post of two dozen 4K screenshots readable rather than a scroll.

So this is the host for a set of screenshots that is going into a post. For a comparison it is the wrong shape and kiyas says so before sending anything, rather than refusing: a screenshot set of two releases is a real thing to want hosted.

Four things to know:

  • No account and no key. The API does not take one, so there is nothing to store and nothing to configure.
  • 10 MB per image. A 4K PNG passes that regularly — measured at 7.3 MB on a UHD HDR10 frame, which is close enough that a busier frame will not fit. kiyas checks every file before sending anything and names the ones that are over, because twenty images can go up before the twenty-first is refused.
  • No unlisted mode and no expiry, so --public, --no-optimize, --tmdb and --remove-after have no equivalent and are named as ignored. --nsfw does apply here. A gallery's management link is printed once, because the API hands it out once and nothing can recover it afterwards.
  • The PNG is stored unmodified. pixhost's web optimisation is off by default and kiyas never turns it on, which is the only condition under which a lossless screenshot survives being hosted.

--format comparison and --format img fall back to a single link here: those tags need the address of the picture itself, and the only per-image address pixhost documents is an HTML page. Putting a page inside an [img] tag is a broken picture on every forum there is, so kiyas does not.

Uploads are paced deliberately. slow.pics is free, run by one person, and sits behind Cloudflare, which blocks addresses that arrive in bursts — a 24-image comparison was enough to earn one. Three things keep kiyas under that line, and they matter in this order:

  • Upload starts are held a fixed interval apart across all six workers, and retries go through the same pacing, because a retry is another request arriving at the same server. About ten seconds on a 24-image comparison.
  • A refusal is never retried. Five further attempts against a block cannot succeed and are themselves the traffic that turns a rate limit into a ban.
  • The first refusal stops the whole upload. The answer is about your address, so it is the same answer for every remaining image.

Together those turn the worst case for a 24-image comparison from around 240 requests into at most six.

If your address does get blocked, kiyas says so in one sentence: which Cloudflare error it is and what it means. Both kinds lapse on their own — an address refused with Cloudflare's 1006, whose page says the owner "has banned your IP address", was serving requests again the same day — so the answer is to leave it a while rather than to keep trying. Nothing is lost either way: the comparison on disk is untouched, and kiyas publish out/ picks it up unchanged later or from somewhere else.

Roadmap

Phase Scope State
0 Environment setup, diagnostics, binary resolution done
1 Source model, project TOML, frame selection, VapourSynth + ffmpeg engines, tonemapping done
2 slow.pics and comp.pics upload, forum markup done
3 mpv layer: portable config dir, frame picker, settings comparison done
4 Audio: spectrograms, waveforms, frequency response, bit depth, offset, metadata table done
5 PySide6 desktop interface done
6 Packaging, CI, release done

Credits

kiyas would not exist without prior work by other people:

  • Solarios — whose Install VapourSynth and run screenshot comparisons guide documented the pip-installable VapourSynth stack and the B-frame selection rule kiyas uses.
  • afunnylookingsquash — Squash-P2P-Scriptorium, whose multi_comps.vpy defines the per-source parameter model and the order operations have to be applied in, and whose audio scripts are the model for kiyas' audio analysis.
  • Jaded Encoding Thaumaturgy — vsview, vstools, and the plugin ecosystem.
  • awsmfunc — FrameInfo, fixlvls, MapDolbyVision.
  • Slowpoke Pics — for hosting comparisons for free.
  • DejaVu Fonts -- the label font kiyas ships, under the Bitstream Vera and Arev licences. See src/kiyas/assets/DejaVuSans.LICENSE.

License

GPL-3.0. See LICENSE.

About

A comparison workbench for video and audio releases: frame-matched screenshots, player-settings comparisons, audio analysis, and publishing to slow.pics or comp.pics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages