Elevator pitch: Redslip is Monday triage for a film archive: ranks failing masters, lanes each BATCH, HUMAN or READY (11/6/3), ranking on 320 rollup rows while forensics stay on 24,017 ClickHouse samples.
About the project
Five slots this product owns
| Slot | Redslip owns |
|---|---|
| Incident | Monday at an archive digitization desk: twenty masters sitting in a drop folder, and no page that ranks which ones a streamer would reject today |
| Named user | Jordan, archive digitization / catalog delivery lead who allocates mix-stage hours across a fleet, not the operator closing one outbound file |
| Named incumbent | Telestream Vantage, Venera Pulsar, Interra Baton: excellent per-file report stacks that never answer "which of these do I touch first this week" |
| Measured number | 11 BATCH / 6 HUMAN / 3 READY across 20 titles; ranking reads 320 rollup rows in 16.5 ms while the forensic stream behind them holds 24,017 ClickHouse samples |
| Honest limit | Redslip measures, ranks and dispatches. It does not repair. A BATCH lane is a work order, not a rewritten WAV |
That is a different product from a single-master measure-repair-remeasure loop. Same ClickHouse track, opposite verb: triage a corpus, do not close one file.
How the page is built
The document below follows the product: the catalog first, then one row out of it, then the raw measurement underneath that row. Every claim carries the statement that produced it and what the statement cost to run.

Level one: the catalog
Twenty public-domain features from archive.org, each measured live with ffmpeg, ranked by absolute distance from the EBU R128 target of -23.0 LUFS.
| Title | Integrated | LU from target | Verdict | Lane |
|---|---|---|---|---|
| Fugitive Valley | -37.2 LUFS | 14.2 | FAIL | BATCH |
| Outpost In Morocco | -15.8 LUFS | 7.2 | FAIL | BATCH |
| quevadis | -16.5 LUFS | 6.5 | FAIL | BATCH |
| Farewell to Arms, A | -16.5 LUFS | 6.5 | FAIL | BATCH |
| Romance on the Run | -16.9 LUFS | 6.1 | FAIL | BATCH |
| Night Tide, corrected audio | -17.4 LUFS | 5.6 | FAIL | BATCH |
| Follow Your Heart | -18.2 LUFS | 4.8 | FAIL | BATCH |
| framed | -20.1 LUFS | 2.9 | FAIL | HUMAN |
| Crimson Romance | -25.9 LUFS | 2.9 | FAIL | BATCH |
| Rough Riding Ranger | -20.3 LUFS | 2.7 | FAIL | BATCH |
| Fit for a King | -21.0 LUFS | 2.0 | FAIL | BATCH |
| Vicki (1953) | -24.9 LUFS | 1.9 | FAIL | HUMAN |
| The Shadow Strikes | -24.8 LUFS | 1.8 | FAIL | HUMAN |
| Successful Failure | -24.5 LUFS | 1.5 | FAIL | HUMAN |
| Song for Miss Julie, A | -21.5 LUFS | 1.5 | FAIL | BATCH |
| Werewolf In A Girls' Dormitory | -22.0 LUFS | 1.0 | PASS | READY |
| Night of the Living Dead (1968) | -24.0 LUFS | 1.0 | FAIL | HUMAN |
| What Becomes Of The Children? | -23.9 LUFS | 0.9 | PASS | READY |
| Niagara Falls | -22.3 LUFS | 0.7 | FAIL | HUMAN |
| Night Of The Living Dead, 720p | -22.4 LUFS | 0.6 | PASS | READY |
Seventeen fail, three pass. The three passes matter more than they look: a gate that fails everything is not measuring anything.
The statement that produces that table, and what the server charged for it:
SELECT title_id, title, verdict, lane, failures, integrated_lufs, lufs_delta
FROM vault.fleet
ORDER BY lufs_delta DESC
LIMIT 12
| Measure | Value |
|---|---|
| Rows read | 320 |
| Bytes read | 11,372 |
| Rows returned | 12 |
| Elapsed | 16.5 ms |
Ordering is absolute distance in LU, not raw LUFS. Sorting on raw LUFS puts every too-loud title at one end of the list and every too-quiet one at the other, which sorts by the direction of the error rather than its size. Fugitive Valley at 14.2 LU stays first whether the next title is loud or quiet.
Level two: one row
Take Night Tide. Five spec checks run against every title, and this one fails two.
| Check | Spec | Measured | Verdict |
|---|---|---|---|
| Integrated loudness, EBU R128 | -23.0 LUFS, tolerance 1.0 LU | -17.4 LUFS | 5.6 LU out |
| Integrated loudness, ATSC A/85 | -24.0 LKFS, tolerance 2.0 LU | -17.4 LKFS | 6.6 LU out |
| True peak | ceiling -1.0 dBTP | under ceiling | pass |
| Black segments | none at or over 2 s | two segments | needs a person |
| Frozen video | no freeze events | none | pass |
Night Tide's own slip prints its source command from vault.sources. The worst title
in the ledger is Fugitive Valley, and that is the one a stranger can check without
cloning anything:
ffmpeg -hide_banner -nostats -t 120 \
-i "https://archive.org/download/fugitive_valley/fugitive_valley_512kb.mp4" \
-af ebur128 -f null -
That prints:
Integrated loudness:
I: -37.2 LUFS
Threshold: -47.3 LUFS
which is the figure the slip for Fugitive Valley carries, arrived at by a route that shares no code with the pipeline.

Level three: underneath the row
Night Tide's two black segments look identical in vault.events. They are not the same
thing at all, and the difference is not in the event row. It is in what the audio was
doing at the instant the picture went black.
SELECT e.title_id, e.kind, e.start_seconds, e.end_seconds, s.short_term
FROM vault.latest_events AS e
ASOF LEFT JOIN (
SELECT title_id, t_seconds, toNullable(short_term) AS short_term
FROM vault.loudness_samples
) AS s
ON e.title_id = s.title_id AND s.t_seconds <= e.start_seconds
WHERE e.title_id = 'NightTide16x9CorrectedAudio'
ORDER BY e.start_seconds
| Kind | Start | End | Short-term loudness at onset | Reading |
|---|---|---|---|---|
| silence | 0.02 s | 2.76 s | null | no sample before it, so unmeasured |
| black | 1.17 s | 2.80 s | -120.7 dB | silence under the black: a reel change |
| black | 5.33 s | 6.97 s | -42.0 dB | programme audio under the black: wants a person |
Same table, same two rows, opposite dispositions. Only the point-in-time join separates
them, and the toNullable is load-bearing: an ASOF LEFT JOIN that matches nothing
fills a Float32 with its type default of 0.0, which reads as digital full scale. A
miss has to arrive as null and be reported as unmeasured, which is what the first row
above shows.
The stream that join reads is the expensive one, and the ledger says so:
SELECT title_id,
quantile(0.1)(short_term) AS p10,
quantile(0.5)(short_term) AS p50,
quantile(0.9)(short_term) AS p90,
max(short_term) - min(short_term) AS spread_lu
FROM vault.loudness_samples
WHERE short_term > -70
GROUP BY title_id
ORDER BY spread_lu DESC
LIMIT 8
| Measure | Ranking query | Percentile query |
|---|---|---|
| Rows read | 320 | 24,017 |
| Bytes read | 11,372 | 120,680 |
| Rows returned | 12 | 8 |
| Elapsed | 16.5 ms | 5.5 ms |
That gap is the whole storage argument. ebur128 emits a reading every 100 milliseconds,
so a 90-minute feature is roughly 54,000 rows, and this 20-title scan over a
120-second window is 24,017. Ranking the catalog costs 320 rows regardless, because it
reads a rollup whose size is a function of how many titles exist rather than how much
audio was measured. The percentiles are computed by ClickHouse across the full stream,
which is why they are percentiles rather than a number somebody typed into a cell.
Both figures are read from ClickHouse's own response summary, not counted in Python, and the capture is committed:
source scripts/cloudenv.sh
.venv/bin/python scripts/capture_query_cost.py # writes docs/evidence/query-cost.txt

Inspiration
Incident. An archive digitization lead opens the week with a drop folder of masters and a mix-stage calendar that only has hours for a handful of them. Rejections, when they come, arrive as separate emails to separate people, weeks apart. Nobody ever sees the fleet as one ranked list, so the same class of failure burns the same hours over and over.
Named user. That digitization / catalog delivery lead. Their job is allocation: which titles burn mix-stage time this week, which ones a gain pass can clear unattended, which ones already ship. They are not the person sitting on one outbound file with a loudness meter.
Named incumbent. Telestream Vantage, Venera Pulsar and Interra Baton. Each answers "is this file good" with a thick per-file report. None of them answer "of these two hundred, which twelve do I touch first, and which of those twelve can a machine take". A stack of PDFs is not a ranked work queue.
Concrete case anyone can open. NightTide_512kb.mp4 on archive.org measures at
I: -17.4 LUFS over its first 120 seconds. EBU R128 wants -23.0 LUFS with 1.0 LU of
tolerance, so that master is 5.6 LU out. Fugitive Valley sits 14.2 LU off. Those are
inputs to a ranking, not the product. The product is the ordered fleet and the lane on
each row.
Measuring one master was never the hard part. ebur128 has told you a title sits 14 LU
off target for years, for free. The hard part is holding a catalog of those measurements
so the ordering question stays cheap while the forensic question stays possible. Ordering
wants one small row per title. Forensics want every 100 ms reading ffmpeg ever emitted.
That tension is what a column store is for, and it is why Redslip's ranking query never
touches vault.loudness_samples.
What it does
Point Redslip at an archive and it returns a red slip per title: the failed check, the measured number against the spec it failed, the timecode to listen to, and the bay the title goes to. It stops there. The slip is a work order. The WAV is untouched.
- BATCH every failure on the title is auto-fixable and the loudness profile is flat,
so an unattended
loudnormpass can correct it. - HUMAN the title needs a person: a structural defect, or a wide loudness profile where a single gain change would bury the dialogue while fixing the average.
- READY no failed checks. It ships.
The split across the measured catalog is 11 BATCH, 6 HUMAN, 3 READY.
Five checks run against every title, giving 100 finding rows:
| Check | Titles failing |
|---|---|
| Integrated loudness, EBU R128 | 15 |
| Integrated loudness, ATSC A/85 (CALM Act) | 11 |
| Black segments at or over 2 s | 5 |
| Frozen video | 2 |
| True peak ceiling, -1.0 dBTP | 0 |
The decisions land back in the database. vault.slips is an append-only log of every
work order the crew has issued, one row per queued title with its lane, its rationale and
a flag recording whether each figure was traced to a returned cell, so the queue outlives
the browser tab and the next scan can be read against the last decision. Read back with
SELECT count(DISTINCT run_id), count() FROM vault.slips, it held 11 runs and 124 rows at
the time this was written.
What the ledger will not infer
The hard product limit first: Redslip does not rewrite audio or subtitles. BATCH means "safe for an unattended gain pass somewhere else"; it is not a repaired file inside this service. A sibling product on the same track owns measure, repair and re-measure of one master. Redslip owns the fleet question that product cannot answer.
Five further questions have an obvious machine answer that would be wrong, so Redslip does not answer them either.
A 120-second window is a 120-second window. Every title is measured over its first two minutes, and every figure carries that window in the interface and in the API payload. A title can normalise cleanly across an opening reel and drift later. The number on the slip is what the scan saw, and it is never presented as a full-length measurement.
A black frame is never auto-repaired. A reel change, a fade and physical damage look identical to a machine at the event-row level. Redslip stamps each one with the loudness at its onset and routes the ambiguous ones to a person, because that judgement is a person's.
A missing loudness sample is reported as unmeasured, not as zero. The toNullable in
the ASOF join exists so that "no reading at that instant" cannot be rendered as a level.
The projection is labelled a projection. archive.org holds 28,423 public-domain
titles. Scanning them all at full length would put roughly 27 million rows in
vault.loudness_samples. That sentence is arithmetic from the 100 ms emission rate, it
says so wherever it appears, and 20 titles are what has been measured.
Subtitle checks are code, not findings. Netflix TTSS reading speed, minimum cue duration and line length are implemented and covered by tests. None of the 20 titles in this catalog ships an SRT, so those checks contribute zero rows to every number above, and the catalog totals are not padded with them.
How we built it
Ingestion is deterministic and holds no model. ingest.py runs ffprobe for stream
metadata, then ffmpeg with ebur128 for loudness, then blackdetect, freezedetect
and silencedetect for structural defects, then parses any SRT against Netflix TTSS
limits. Spec verdicts land in vault.findings, defect occurrences in vault.events, the
raw series in vault.loudness_samples, and the measured URL in vault.sources. No model
touches any of it, because a measurement a model can influence is not a measurement.
The schema is laid out for the shape of the data. Gorilla then ZSTD(3) on the
loudness floats, because Gorilla is the XOR-of-successive-values codec built for a slowly
varying float series. DoubleDelta on the scan clock. LowCardinality(String) on
title_id. PARTITION BY cityHash64(title_id) % 8, so re-scanning one title after a
repair is a partition drop rather than a mutation, which is what a re-delivery actually
is.
Ranking never reads the sample stream. vault.title_loudness is an
AggregatingMergeTree keyed on title_id, kept current by a materialized view whose
GROUP BY is the rollup's ORDER BY, which is the condition for the merge to stay
incremental. Ingesting title 21 merges one part in; it does not rebuild the fleet.
vault.fleet reads that rollup, one row per title, joined to the 100-row findings table.
That is checkable rather than argued. EXPLAIN indexes = 1 on the ranking query is
asserted to name vault.findings and vault.title_loudness and never
vault.loudness_samples, in tests/test_ranking_never_scans_samples.py. A companion
test in the same file runs a query that genuinely scans the samples and asserts that
EXPLAIN does name the table then, which is what stops the first assertion from being a
check that cannot fail. The captured plan, taken through the MCP server against ClickHouse
Cloud with its control query beside it, is committed at
docs/evidence/explain-ranking-plan.txt.
The crew is Google ADK, and the graph is the argument.
SequentialAgent redslip_triage
├── LlmAgent fleet_scout McpToolset -> mcp-clickhouse
│ reads vault.fleet, ranks the catalog, chooses how deep to cut
│ output_key: fleet
├── ParallelAgent evidence
│ ├── LlmAgent loudness_analyst McpToolset -> mcp-clickhouse
│ │ reads vault.loudness_samples for the queued titles only
│ │ output_key: loudness_evidence
│ └── LlmAgent structural_analyst McpToolset -> mcp-clickhouse
│ reads vault.latest_events ASOF vault.loudness_samples
│ output_key: structural_evidence
└── LlmAgent work_allocator no database tools at all
output_schema pinned to a pydantic WorkOrder
output_key: work_order
Every agent changes an outcome the others cannot reach. The scout decides the length of the queue, which is a judgement about how much mix-stage time exists this week. The loudness analyst decides whether a failure is repairable by gain, which is the difference between BATCH and HUMAN and is in no column. The structural analyst decides whether a black frame is damage, which needs a join no verdict row contains. The allocator holds the ordering and the lane assignment, and holds no tools, so it can only quote its colleagues.
The two analysts are parallel because they read different tables and neither needs the other's answer. They are the only pair in the graph where that is true.
GET /api/agents on the live service reports that exact shape, the model, and the
resolved path of the MCP server binary it starts.

ClickHouse is reached only through the official MCP server. Three of the four agents
hold mcp-clickhouse through ADK's McpToolset over StdioConnectionParams, with
tool_filter=["run_query", "list_tables", "list_databases"]. They compose their own SQL;
nothing in the agent layer runs a stored query string. Bulk insertion of the 100 ms
samples stays on clickhouse-connect, because run_query is not an insert path and
pretending otherwise would be worse engineering dressed as better compliance.

The agents cannot write, and that is enforced twice, in two different places.
The client-side gate is before_tool_callback=_guard_tool in agent/crew.py. It strips
line comments, block comments and string literals before matching, so
SELECT 1 --\n; DROP TABLE x cannot walk through a prefix check, and a
WHERE title_id IN (...) list containing the word "insert" is not read as a statement. A
refused call is short-circuited before it reaches the server, recorded as refused, and
the model is told why so it stops retrying.
Our own callback passing our own tests proves very little about the server, so the other end was checked directly. Two probes were sent through a live MCP session, deliberately bypassing the callback, and both were refused by the database:
CREATE TABLE IF NOT EXISTS vault.readonly_probe (probe UInt8) ENGINE = Null
-> Code: 164. DB::Exception: default: Cannot execute query in readonly mode. (READONLY)
INSERT INTO vault.events SELECT * FROM vault.events WHERE 1 = 0
-> Code: 164. DB::Exception: default: Cannot execute query in readonly mode. (READONLY)
vault.events held 29 rows before and 29 rows after, and vault.readonly_probe does not
exist. Both probes are harmless even if the guarantee had failed, because a Null-engine
table stores nothing and an INSERT ... WHERE 1 = 0 selects no rows: a probe that would
damage the archive if the guarantee failed is not a way to test the guarantee. The
transcript is committed at docs/evidence/readonly-refusal.txt and reproduced with
.venv/bin/python scripts/capture_mcp_evidence.py.
Verification is a Python equality check, not a second model. verify_against_cells
compares every figure on the work order against the numeric cells MCP actually returned
during the run, keeping each cell in its exact form and rounded to one decimal, so that
quoting -26.14 as -26.1 counts as quoting the database while quoting -19.0 does not. Each
run reports verified and lists any unsupported figure. There is deliberately no critic
agent: checking whether the allocator invented a number is an equality test, so it is
written as one.
Gemini is required. Grafana is not. With no model credentials run_crew raises
GeminiRequired and produces nothing, and tests/test_gemini_required.py covers the
crew, the synchronous entry point, and a scan of the whole agent package for a
reintroduced canned plan. A fifth agent publishes the finished work order to Grafana
Cloud through the official mcp-grafana server, and that coupling is structural rather
than a toggle: with no token the agent is not in the graph at all and the run reports the
step as skipped, with a reason and a null publish payload; with a token it is in the
graph and is not optional. tests/test_grafana_is_optional.py asserts both graph shapes,
asserts the ClickHouse crew is identical either way, and asserts /api/agents still
reports ready: true on ClickHouse readiness alone. The deployed service carries no
Grafana token, so that is the path a judge actually runs.
The web layer is FastAPI streaming each composed statement as server-sent events, deployed on Cloud Run against ClickHouse Cloud.

A run against the live service: run f8ebab9d120f, gemini-2.5-flash, 70.1 s end to
end, 12 rows written to vault.slips, every figure traced back to a query result before
it was drawn.
Challenges we ran into
The suite reported 91 passed while never touching ClickHouse. On a clean checkout
with no credentials, pytest tests/ -q printed 91 passed, 32 skipped in 2.40s and
exited 0, which reads as a pass. All 32 skips were the tests that read the measured
catalog, which is the integration this entry rests on. The tally could not distinguish "nothing is
broken" from "nothing was tried", because pytest counts a skip as a non-failure.
Pointed at the real catalog, the same command reports:
$ source scripts/cloudenv.sh # do not pipe it, a subshell discards the exports
$ .venv/bin/python -m pytest tests/ -q
catalog: ClickHouse integration exercised against ctmiqkvdfg.us-central1.gcp.clickhouse.cloud
117 passed, 6 skipped, 22 warnings in 137.55s (0:02:17)
| Run | Result | Wall time |
|---|---|---|
| Bare clone, no credentials | 91 passed, 32 skipped, exit 0 | 2.40 s |
| Pointed at the measured catalog | 117 passed, 6 skipped, exit 0 | 137.55 s |
CLICKHOUSE_HOST set to an unreachable host |
91 passed, 18 skipped, 14 errors, exit 1 | 3.79 s |
Twenty-six tests moved from skipped to passing, and the runtime went up by a factor of
- That factor is the point: the fast run was fast because it was not doing the work.
The fix, in tests/conftest.py, splits the two cases that used to look the same. A bare
clone with no CLICKHOUSE_HOST still skips, because a fresh clone should not read as a
broken project. A run that names a remote host is asserting the catalog is there, so the
gate failing is now a broken configuration and errors instead of skipping, which is the
third row of that table. Either way pytest_terminal_summary states outright whether the
integration was exercised, in green when it was and in red when it was not.
Two MCP SDKs that could not share a room. ADK's McpToolset is built against
mcp<2. The official mcp-clickhouse package pulls fastmcp, which requires mcp>=2.
There is no pin that satisfies both, and time went into looking for one before the
premise turned out to be wrong. An MCP server is a separate process by design. It does
not need to be importable from our environment, it only needs to exist as an executable.
uv tool install mcp-clickhouse gives it its own environment and the conflict stops
existing rather than getting negotiated. The Dockerfile does the same at
/usr/local/bin/mcp-clickhouse, which is the path /api/agents reports.
An ordering that looked right and was wrong. The first ledger sorted on raw LUFS. It produced a plausible-looking list that ranked a title 1 LU too quiet above one 7 LU too loud. Switching to absolute distance from target changed which titles a QC lead touches first, which is the entire output of the product.
An ASOF join that reported silence as full scale. ASOF LEFT JOIN fills an unmatched
right-hand Float32 with 0.0, and 0.0 in this column means digital full scale. An
event starting before the first loudness sample came back looking like the loudest
possible signal, which is the exact inverse of the truth. toNullable(short_term) in the
sub-select makes a miss arrive as null, and the agents are instructed to report a null as
unmeasured rather than as a level.
Accomplishments that we're proud of
The access pattern is checkable rather than asserted. EXPLAIN indexes = 1 naming the
rollup and not the sample stream is a claim anyone can run, and the companion test
proving EXPLAIN does name the samples table when a query reads it is what stops the
first test from being a check that cannot fail.
The read-only guarantee is enforced by ClickHouse, not by our own callback. Two probes
sent through a live MCP session came back Code 164 READONLY, and 29 rows survived both.
One number was confirmed by a route that shares nothing with the pipeline: an independent
ffmpeg run against the public source measured Fugitive Valley at I: -37.2 LUFS,
matching the stored finding exactly.
The test suite refuses to report green on an integration it did not exercise, and says so in the terminal summary in red. That was built after catching the suite doing exactly that.
What we learned
An API that returns without an error has accepted your request, and that is all it has done. The same goes for a test suite: a tally that counts a skip as a non-failure is a green light with no information in it.
Giving an agent fewer tools made it more trustworthy. The allocator, which produces the actual output, is the one agent in the graph with no database access, so the worst it can do is misquote colleagues whose statements were recorded.
Deciding not to build the obvious agent was the better call twice over. A critic agent auditing the allocator does the work of a short equality test, and a second model would have added latency and one more thing that can hallucinate.
The version conflict was the most useful mistake, because pinning was never going to work and the process boundary was sitting there the whole time.
What's next for Redslip
Full-length scans instead of the 120-second window, which is roughly six minutes of
ffmpeg per title and a partition drop per re-scan under the current layout. A catalog
that ships SRTs, so the TTSS path earns findings rather than only tests. Handing a BATCH-lane title to a loudnorm pass stays out of scope on purpose: that closed
repair loop is Checkprint's verb on this track, not Redslip's. Redslip measures, ranks and
dispatches, and the slip says so.
Built with
Google ADK (google-adk), Gemini 2.5 Flash, Google Cloud Run, ClickHouse Cloud, the
official mcp-clickhouse MCP server over stdio via ADK McpToolset, the official
mcp-grafana server for the optional board, clickhouse-connect for bulk sample
ingestion, ffmpeg (ebur128, blackdetect, freezedetect, silencedetect) and
ffprobe, Python 3.13, uv, FastAPI with server-sent events, pytest, Docker, archive.org
as the source catalog, and vanilla HTML and CSS with Newsreader and JetBrains Mono.
Log in or sign up for Devpost to join the conversation.