Skip to content

Commit 87b173d

Browse files
committed
test: add instant audio benchmarks and measured results
1 parent 0fb9dc0 commit 87b173d

5 files changed

Lines changed: 535 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Instant audio quality experiment
2+
3+
This is an offline, shadow-only experiment. No route, recording finalizer, player,
4+
export, upload, desktop capture path, or production flag imports the worker.
5+
`mode: "off"` returns before filesystem access. There is no publishing mode.
6+
7+
The proposed first rollout is bounded, constant level correction, after production
8+
validation. EQ and denoising remain experimental because consistent perceptual
9+
improvement has not been established. This PR does not enable either profile.
10+
11+
## Evidence and limits
12+
13+
The September 7–8, 2026 study measured 60 additional public, unprotected Instant
14+
recordings with completed transcripts: 20 from each of September 5, 6, and 7,
15+
all from different owners and separate from the initial 12-recording study.
16+
The expanded cohort contains 4.464 hours of audio. Median playback-compensated
17+
loudness is -29.205 LUFS; 40/60 recordings are below -24 LUFS, 9 already exceed
18+
-18 LUFS, and 9 have true peaks above 0 dBTP.
19+
20+
A fixed 39-recording tuning / 21-recording holdout split preceded processing.
21+
The final voice policy passed technical gates on 42/60, left 17 unchanged, and
22+
rejected one short holdout clip for excessive gain. Among the 42 passing clips,
23+
median loudness moved from -31.745 to -16.84 LUFS, median gain was 14.495 dB,
24+
and the highest encoded true peak was -1.34 dBTP. All decoded sample counts were
25+
preserved; container duration changes were at most 21 ms. The reserved holdout
26+
alone had 15 passes, five unchanged, and that one rejected candidate.
27+
Constant-gain processing passed 40/60 and left 20 unchanged, with no rejected
28+
outputs. These counts measure technical eligibility, not listening preference.
29+
30+
Five initial policies produced 195 comparisons. Naive dynamic normalization,
31+
EQ/compression, and denoising each changed container duration by more than 25 ms
32+
on 29/39 recordings. Aggressive processing also damaged synthetic intelligibility
33+
scores. These failures remain in the local evidence; they are not shipping presets.
34+
35+
The adjusted policy uses a 60 Hz high-pass, -0.75 dB at 250 Hz, +0.75 dB at 2.5 kHz,
36+
6 dB adaptive FFT denoising, and loudness normalization. It has no extra compressor.
37+
Input and output gates bound gain and peak level. Separate constant-gain processing
38+
is available for content whose suitability for voice processing is unknown.
39+
40+
The exact final worker was tested on 39 controlled cases at 48 kHz: three reference
41+
voice excerpts, four additive noise types, and three SNRs, plus the unmodified
42+
references. Median STOI change was -0.000260, worst -0.005318; none exceeded the
43+
chosen -0.01 regression tolerance. Six inputs were conservatively left unchanged.
44+
These are relative tests against existing recordings, not clean studio ground
45+
truth, subjective quality ratings, or a matched Loom comparison. The calibration
46+
run with 12 dB denoising exceeded that tolerance in four cases, motivating 6 dB.
47+
48+
Volume-matched RMS in uncaptioned intervals changed by a median +0.096 dB across
49+
41 passing clips, with a maximum increase of 6.841 dB. Twelve voice candidates
50+
changed LRA by more than two LU. Those observations require listening review for
51+
background noise swelling and altered dynamics before enabling voice processing.
52+
53+
Full-recording LUFS and caption-aligned RMS answer different questions. Caption
54+
intervals approximate speech activity; uncaptioned audio is not necessarily noise
55+
or silence. A completed transcript does not establish that a recording contains
56+
only microphone speech. Mono loudness uses FFmpeg's `dual_mono=true` playback
57+
compensation consistently; it must not be mixed with uncompensated mono metrics.
58+
59+
## Source and timing guarantees
60+
61+
The worker only reads an absolute regular local source, hashes it before and after,
62+
and writes into a unique temporary directory. It copies video packets and verifies
63+
them with the existing packet-proof helper. Existing finalization checks are
64+
unchanged. Results carry source/output hashes, metrics, version, and validation
65+
failures; every nonempty validation failure list disqualifies that candidate.
66+
67+
The worker skips silence, extreme levels, existing clipping, unsupported formats,
68+
already loud content, nonzero audio start times, discontinuous source timestamps,
69+
and mismatched source audio/video durations. Voice processing additionally requires
70+
`speechOnlyConfirmed`; the benchmark explicitly overrides this only for local
71+
research. There is no production content classifier in this change.
72+
73+
FFmpeg's denoiser delays content by two sample-advance blocks without adjusting
74+
PTS. Padding the tail and trimming that delay preserves boundary speech. Integer
75+
sample timebases avoid timestamp rounding at 44.1 kHz. The encoded AAC result is
76+
remeasured, with one bounded peak correction rendered from the original if needed.
77+
Failed validation never authorizes publication. Cancellation, timeouts, and exceptions
78+
clean up only the worker's own temporary files.
79+
80+
26 tests cover policy gates, mono/stereo, 44.1/48 kHz, both profiles, speech-like
81+
markers at clip boundaries, exact video packets, source preservation, silence,
82+
nonzero/discontinuous timestamps, cancellation, and timeout. Scoped TypeScript and
83+
Biome checks also pass. Measurements used macOS FFmpeg 8.0.1 and Bun 1.4.0;
84+
production Linux/FFmpeg behavior has not been validated.
85+
86+
## Reproducing
87+
88+
Keep source media, transcripts, per-recording measurements, and customer identifiers
89+
outside the repository. Aggregate results and the frozen worker source hash are in
90+
[audio-quality-benchmark-summary.json](audio-quality-benchmark-summary.json).
91+
The local study retains `final-summary.json`, `final-voice-results.json`,
92+
`final-levels-results.json`, and `report.md`, plus per-recording run receipts.
93+
Interrupted runs and retries are retained separately.
94+
95+
The manifest is a JSON array with `id`, `split` (`tuning` or `holdout`), `stratum`,
96+
`createdAt`, and `duration`. Sources are `sources/<id>.m4a`; transcripts are
97+
`sources/<id>.vtt`. Initial download uses the authenticated Cap CLI for existing
98+
transcripts and the public playlist for audio. No new transcription is requested.
99+
100+
```sh
101+
python3 scripts/benchmark-instant-audio.py /absolute/study --phase baseline
102+
python3 scripts/benchmark-instant-audio.py /absolute/study --phase tuning --policies gain6 gain12 dynamic equalized clean
103+
bun apps/media-server/scripts/benchmark-audio-quality.ts /absolute/study tuning unique-label voice
104+
bun apps/media-server/scripts/benchmark-audio-quality.ts /absolute/study holdout another-label levels
105+
```
106+
107+
Use a new label per run; the worker benchmark will not overwrite existing results.
108+
The intelligibility calibration requires NumPy, SciPy, and pystoi. Supply three
109+
reference IDs with `--reference-ids`; their M4A files must be two directories above
110+
the output directory. Output-directory suffix `-v2` selects the corrected mild
111+
policy; a name containing `strength` selects the 6/12 dB comparison. This calibration
112+
script records historical filter alternatives; the TypeScript worker benchmark is
113+
the authoritative final implementation.
114+
115+
## Before serving any enhanced audio
116+
117+
Human review of the 12 volume-matched A/B excerpts is still required. Speech-only
118+
eligibility, noisy and mixed-system-audio cases, and recordings excluded by the
119+
public/transcribed selection need broader coverage. The short holdout clip rejected
120+
for excessive gain must remain on its original audio; do not relax its gate to make
121+
the benchmark pass.
122+
123+
Run the exact policy in the production Linux image, then verify actual share-page,
124+
embed, seeking, downloads, edits, transcript alignment, and fallback behavior.
125+
Measure worker memory, throughput, storage, and tail latency before rollout.
126+
127+
Future integration should create a separately versioned derivative after the
128+
original is available, using a durable idempotent job bound to the source hash.
129+
Publish atomically only after validation and only if the source still matches.
130+
Keep the original available throughout processing, on failure, and for rollback.
131+
Existing desktop installs could then benefit server-side without a capture update;
132+
this experiment does not yet implement that serving integration.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"baseline": {
3+
"count": 60,
4+
"hours": 4.463996666666667,
5+
"medianLufs": -29.205,
6+
"belowMinus24": 40,
7+
"peakAboveZero": 9,
8+
"alreadyLoud": 9
9+
},
10+
"voice": {
11+
"count": 60,
12+
"passing": 42,
13+
"rejected": 1,
14+
"unchanged": 17,
15+
"errors": [],
16+
"skipReasons": {
17+
"unsafe-levels": 12,
18+
"already-loud": 5
19+
},
20+
"medianInputLufs": -31.744999999999997,
21+
"medianOutputLufs": -16.84,
22+
"medianGain": 14.495,
23+
"maxTruePeak": -1.34,
24+
"maxDurationDeltaMs": 20.999999999958163,
25+
"sampleCountsExact": true,
26+
"medianRealtimeFactor": 0.08116900157594112
27+
},
28+
"levels": {
29+
"count": 60,
30+
"passing": 40,
31+
"rejected": 0,
32+
"unchanged": 20,
33+
"errors": [],
34+
"skipReasons": {
35+
"unsafe-levels": 12,
36+
"already-loud": 5,
37+
"insufficient-headroom": 3
38+
},
39+
"medianInputLufs": -32.07,
40+
"medianOutputLufs": -23.08,
41+
"medianGain": 10.64,
42+
"maxTruePeak": -1.95,
43+
"maxDurationDeltaMs": 20.999999999958163,
44+
"sampleCountsExact": true,
45+
"medianRealtimeFactor": 0.09984551719406805
46+
},
47+
"holdoutVoice": {
48+
"count": 21,
49+
"passing": 15,
50+
"rejected": 1,
51+
"unchanged": 5,
52+
"errors": [],
53+
"skipReasons": {
54+
"unsafe-levels": 2,
55+
"already-loud": 3
56+
},
57+
"medianInputLufs": -30.92,
58+
"medianOutputLufs": -16.78,
59+
"medianGain": 13.339999999999998,
60+
"maxTruePeak": -1.34,
61+
"maxDurationDeltaMs": 20.999999999958163,
62+
"sampleCountsExact": true,
63+
"medianRealtimeFactor": 0.08261003124306479
64+
},
65+
"codeHash": "40ef7500ef8643decd898509879bfe0293ced1f5d124f8a93535f9080f1bf609",
66+
"scope": "Offline public/transcribed cohort; passing technical gates does not establish perceptual quality or production eligibility.",
67+
"productionEnabled": false
68+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { createHash } from "node:crypto";
2+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3+
import { isAbsolute, join } from "node:path";
4+
import { z } from "zod";
5+
import { createAudioQualityCandidate } from "../src/lib/audio-quality";
6+
7+
const [root, split, label, profile] = process.argv.slice(2);
8+
if (
9+
!root ||
10+
!isAbsolute(root) ||
11+
!["tuning", "holdout"].includes(split) ||
12+
!label ||
13+
!/^[a-z0-9-]+$/.test(label) ||
14+
(profile !== "levels" && profile !== "voice")
15+
)
16+
throw new Error(
17+
"Usage: benchmark-audio-quality.ts ABS_ROOT tuning|holdout LABEL levels|voice",
18+
);
19+
20+
const rows = z
21+
.array(
22+
z.object({
23+
id: z.string().regex(/^[a-z0-9]{15}$/),
24+
split: z.enum(["tuning", "holdout"]),
25+
}),
26+
)
27+
.parse(JSON.parse(await readFile(join(root, "cohort.json"), "utf8")))
28+
.filter((row) => row.split === split);
29+
const destination = join(root, label);
30+
await mkdir(destination);
31+
const codeHash = createHash("sha256")
32+
.update(
33+
await readFile(new URL("../src/lib/audio-quality.ts", import.meta.url)),
34+
)
35+
.update(
36+
await readFile(
37+
new URL("../src/lib/audio-quality-policy.ts", import.meta.url),
38+
),
39+
)
40+
.digest("hex");
41+
await writeFile(
42+
join(destination, "run.json"),
43+
JSON.stringify(
44+
{
45+
split,
46+
profile,
47+
codeHash,
48+
count: rows.length,
49+
forcedContentGateForOfflineExperiment: profile === "voice",
50+
startedAt: new Date().toISOString(),
51+
},
52+
null,
53+
2,
54+
),
55+
{ flag: "wx" },
56+
);
57+
let next = 0;
58+
const results: Record<string, unknown>[] = [];
59+
async function worker() {
60+
for (;;) {
61+
const row = rows[next++];
62+
if (!row) return;
63+
const started = performance.now();
64+
let receipt: Record<string, unknown>;
65+
try {
66+
const result = await createAudioQualityCandidate(
67+
join(root, "sources", `${row.id}.m4a`),
68+
{
69+
mode: "shadow",
70+
profile: profile as "levels" | "voice",
71+
speechOnlyConfirmed: profile === "voice",
72+
},
73+
);
74+
if (result.status === "shadow-candidate") {
75+
try {
76+
await copyFile(result.path, join(destination, `${row.id}.mp4`), 1);
77+
const { cleanup: _cleanup, path: _path, ...evidence } = result;
78+
receipt = { id: row.id, ...evidence, codeHash };
79+
} finally {
80+
await result.cleanup();
81+
}
82+
} else receipt = { id: row.id, ...result, codeHash };
83+
} catch (error) {
84+
receipt = {
85+
id: row.id,
86+
status: "failed",
87+
error: error instanceof Error ? error.message : String(error),
88+
codeHash,
89+
};
90+
}
91+
receipt.wallMs = performance.now() - started;
92+
await writeFile(
93+
join(destination, `${row.id}.json`),
94+
JSON.stringify(receipt, null, 2),
95+
{ flag: "wx" },
96+
);
97+
results.push(receipt);
98+
console.log(JSON.stringify(receipt));
99+
}
100+
}
101+
await Promise.all([worker(), worker()]);
102+
await writeFile(
103+
join(destination, "results.json"),
104+
JSON.stringify(results, null, 2),
105+
{ flag: "wx" },
106+
);
107+
if (results.some((result) => result.status === "failed")) process.exitCode = 1;

0 commit comments

Comments
 (0)