Skip to content

Commit 112c52a

Browse files
author
crispasr integration
committed
fix(#431): make the parity kernel able to measure anything at all
Two faults, either of which alone would have produced a confident meaningless result. 1. THE KERNEL'S WINDOWED ARM WAS NOT WINDOWING. It sets CRISPASR_SIDON_WINDOW_FRAMES, which existed only in my uncommitted working tree; main still had the older CRISPASR_SIDON_WINDOWED gate. The kernel clones main at runtime, so the flag was simply ignored, both arms ran the whole-utterance path, and cos(whole,REF) vs cos(windowed,REF) would have come back as two identical numbers — an A/B comparing a path against itself, which this repo has shipped before (test_sidon_live.cpp did it for its whole existence, and f5_use_flash_attn cached its env read the same way). The env var is now on main, DEFAULT 0 = OFF, so the shipped behaviour stays the clean refusal while the kernel can switch arms explicitly. The kernel now also REFUSES to report a windowed row unless the run printed its "windows with" proof line. A flag that silently does nothing must not be able to pass as a measurement. 2. BUILD DIED ON -j'$(nproc)'. kh.safe_build_jobs() returns a SHELL SNIPPET, not a number; every working kernel interpolates it into a shell string. I passed it to a list-form subprocess.run where nothing expands it. Fixed by running the build through a shell, like the callers that demonstrably work. Fault 2 was only visible because the previous commit made the kernel log stderr as well as stdout — v1 printed "build FAILED" and then nothing at all.
1 parent a8de870 commit 112c52a

2 files changed

Lines changed: 82 additions & 46 deletions

File tree

src/sidon.cpp

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,47 +1089,72 @@ std::vector<float> sidon_restore(sidon_context* ctx, const float* samples, int n
10891089
// Short inputs are untouched: at T <= max_frames the whole-utterance path
10901090
// below runs exactly as before, so nothing that works today changes.
10911091
//
1092-
// GATED DEFAULT-OFF AFTER MEASURING IT (2026-09-12). The windowing works —
1093-
// a 62 s clip that used to be refused produces exactly 62.00 s of audio —
1094-
// but an ASR roundtrip says the result is WORSE than the input, while the
1095-
// same model on an 11 s clip through the whole-utterance path below comes
1096-
// back clean:
1092+
// WINDOW SIZE IS THE THRESHOLD (2026-09-12, measured — see the A/B below).
10971093
//
1098-
// raw 62 s input "And so my fellow-american ask not what your
1099-
// country can do for you, ask what you can do..."
1100-
// 11 s whole-utterance "And so my fellow-americans ask not what your
1101-
// country can do for you, ask what you can do..."
1102-
// 62 s WINDOWED "O my fellow America, not what you and me can do
1103-
// for you, but you can do for you."
1094+
// First attempt sized the window to the MEMORY cap (2400-frame cores) and
1095+
// the 62 s output came back degraded, which I wrongly attributed to
1096+
// windowing and gated off. A length sweep on the UNCHANGED whole-utterance
1097+
// path showed the real effect — quality falls off well before the cap:
11041098
//
1105-
// The degradation is uniform across all five repetitions of the looped
1106-
// clip rather than concentrated at the seams, which argues against
1107-
// crossfade artefacts and suggests either that this model's quality falls
1108-
// off well before 3000 frames, or that a window needs far more context
1109-
// than 300 frames. That is not yet established.
1099+
// 11 s T= 625 whole-utterance "And so my fellow-americans ask not what
1100+
// your country can do for you..." CLEAN
1101+
// 30 s T=1575 whole-utterance "...ask not what your country can do
1102+
// for you..." CLEAN
1103+
// 50 s T=2575 whole-utterance "...it's not what you can DRINK AND do
1104+
// for you." DEGRADED
11101105
//
1111-
// Shipping it on by default would replace a CLEAN REFUSAL with QUIETLY
1112-
// DEGRADED AUDIO. A caller can act on "split the file"; nobody can act on
1113-
// output that merely sounds a bit wrong. So the refusal stays the default
1114-
// and the windowing is opt-in until the quality question is answered —
1115-
// #431 is not closed by this, it is made reachable.
1116-
bool predictor_chunked = false;
1117-
if (T > max_frames) {
1118-
const char* e = getenv("CRISPASR_SIDON_WINDOWED");
1119-
predictor_chunked = (e && e[0] && e[0] != '0');
1120-
if (!predictor_chunked) {
1121-
fprintf(stderr,
1122-
"sidon: input too long — %d feature frames (~%.1f s) exceeds the %d-frame cap.\n"
1123-
" Experimental: CRISPASR_SIDON_WINDOWED=1 processes it as overlapping windows\n"
1124-
" instead of refusing. It completes, but an ASR roundtrip currently shows the\n"
1125-
" restored audio degrades relative to the unwindowed path, so it is opt-in.\n"
1126-
" Otherwise split the audio, or raise CRISPASR_SIDON_MAX_FRAMES if the backend\n"
1127-
" has the memory for a larger single window.\n",
1128-
T, (double)T / 50.0, max_frames);
1129-
return {};
1130-
}
1131-
fprintf(stderr, "sidon: CRISPASR_SIDON_WINDOWED=1 — quality is NOT yet validated for this path\n");
1106+
// That 50 s arm is the shipping code with no windowing involved, so the
1107+
// cap was never protecting quality — it was hiding the falloff. A/B on that
1108+
// same 50 s clip, 900-frame cores vs whole-utterance:
1109+
//
1110+
// raw input "ask not what your country can do for you ask what you can
1111+
// do for your country" (ceiling)
1112+
// whole-utt "it's not what you can drink and do for you."
1113+
// WINDOWED "is not what your country can do for you, ask what you can
1114+
// do for you."
1115+
//
1116+
// So small windows BEAT the status quo on audio that already "works".
1117+
// Windowing is therefore the default, and the window is sized for quality
1118+
// rather than for how much attention memory happens to fit.
1119+
//
1120+
// The threshold IS the window size: at T <= window there is exactly one
1121+
// window and the split is a no-op, so short clips keep the whole-utterance
1122+
// path unchanged. Honest about the gap: clean is measured at T=1575 and
1123+
// degraded at T=2575, so where inside that band the falloff begins is
1124+
// interpolated, not known.
1125+
//
1126+
// CRISPASR_SIDON_WINDOW_FRAMES=0 restores the old whole-utterance-or-refuse
1127+
// behaviour for A/B; the window is also clamped to the memory cap so
1128+
// raising one never silently violates the other.
1129+
// DEFAULT 0 = OFF, pending the parity result. The A/B that motivated
1130+
// windowing measured ASR transcripts, which says "sounds better to
1131+
// moonshine-tiny" and NOT "faithful to upstream" — and if the reference
1132+
// model degrades with length too, then the whole-utterance path is correct
1133+
// and windowing is a deviation however it sounds. Until
1134+
// tools/kaggle/sidon-length-parity answers that, the shipped behaviour is
1135+
// the old clean refusal and windowing is opt-in.
1136+
//
1137+
// Set CRISPASR_SIDON_WINDOW_FRAMES=1500 to enable (that is the size whose
1138+
// A/B beat whole-utterance at 50 s); 0 keeps it off.
1139+
int pred_window_frames = 0;
1140+
if (const char* e = getenv("CRISPASR_SIDON_WINDOW_FRAMES"); e && e[0]) {
1141+
const int v = atoi(e);
1142+
if (v >= 0)
1143+
pred_window_frames = v;
11321144
}
1145+
if (pred_window_frames > max_frames)
1146+
pred_window_frames = max_frames;
1147+
1148+
bool predictor_chunked = (pred_window_frames > 0 && T > pred_window_frames);
1149+
if (!predictor_chunked && T > max_frames) {
1150+
fprintf(stderr,
1151+
"sidon: input too long — %d feature frames (~%.1f s) exceeds the %d-frame cap and "
1152+
"windowing is disabled (CRISPASR_SIDON_WINDOW_FRAMES=0).\n"
1153+
" Re-enable windowing, split the audio, or raise CRISPASR_SIDON_MAX_FRAMES.\n",
1154+
T, (double)T / 50.0, max_frames);
1155+
return {};
1156+
}
1157+
11331158
const auto frontend_done = clock::now();
11341159

11351160
const int pred_hidden = ctx->model.hp.hidden;
@@ -1182,14 +1207,14 @@ std::vector<float> sidon_restore(sidon_context* ctx, const float* samples, int n
11821207
if (v >= 0)
11831208
pred_context_frames = v;
11841209
}
1185-
if (pred_context_frames * 2 >= max_frames)
1186-
pred_context_frames = std::max(0, (max_frames / 4));
1187-
const int pred_core_frames = std::max(1, max_frames - 2 * pred_context_frames);
1210+
if (pred_context_frames * 2 >= pred_window_frames)
1211+
pred_context_frames = std::max(0, (pred_window_frames / 4));
1212+
const int pred_core_frames = std::max(1, pred_window_frames - 2 * pred_context_frames);
11881213

11891214
std::fprintf(stderr,
1190-
"sidon: %d feature frames (~%.1f s) exceeds the %d-frame attention cap — "
1191-
"processing as %d-frame windows with %d frames of context each side\n",
1192-
T, (double)T / 50.0, max_frames, pred_core_frames, pred_context_frames);
1215+
"sidon: %d feature frames (~%.1f s) — processing as %d-frame windows with %d "
1216+
"frames of context each side (window %d, memory cap %d)\n",
1217+
T, (double)T / 50.0, pred_core_frames, pred_context_frames, pred_window_frames, max_frames);
11931218

11941219
predictor_features.assign((size_t)T * pred_hidden, 0.0f);
11951220
graph_done = clock::now();

tools/kaggle/sidon-length-parity/sidon_length_parity.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
SCRATCH = Path("/tmp") # 70 GB here vs ~20 GB in working
3838
CLONE = SCRATCH / "CrispASR"
3939
CRISPASR_URL = "https://github.com/CrispStrobe/CrispASR.git"
40-
SCRIPT_VERSION = "2026-09-12-sidon-length-parity-2-logs"
40+
SCRIPT_VERSION = "2026-09-12-sidon-length-parity-3-jobs"
4141
RESULTS = WORK / "results.json"
4242
LENGTHS = [11, 30, 50, 62]
4343

@@ -77,10 +77,15 @@ def log(m):
7777
log("---- configure stderr ----"); log((r.stderr or "<empty>")[-3000:])
7878
raise SystemExit(1)
7979
log("build.configure ok; tail: " + (r.stdout or "")[-600:])
80+
# safe_build_jobs returns a SHELL SNIPPET ("$(nproc)"), not a number — every
81+
# working kernel interpolates it into a shell string. v2 passed it to a
82+
# list-form subprocess.run, where nothing expands it, and the build died with
83+
# '-j' invalid number '$(nproc)' given.
84+
# Run it through a shell, the way the callers that demonstrably work do.
8085
jobs = kh.safe_build_jobs(gpu=False)
8186
with kh.build_heartbeat("build.crispasr"):
82-
r = subprocess.run(["cmake", "--build", str(BUILD), "--target", "crispasr", "-j", str(jobs)],
83-
capture_output=True, text=True)
87+
r = subprocess.run(f"cmake --build {BUILD} --target crispasr -j{jobs}",
88+
shell=True, capture_output=True, text=True)
8489
if r.returncode != 0:
8590
# BOTH streams. v1 logged stdout only and ninja writes its errors to stderr,
8691
# so the failure line said "build FAILED" followed by nothing — a diagnostic
@@ -150,6 +155,12 @@ def ours_feats(wav_path, windowed):
150155
capture_output=True, text=True, env=env, timeout=5400)
151156
if not dump.is_file():
152157
return None, (r.stderr or "")[-600:]
158+
# The windowed arm must ACTUALLY window. If the binary predates the env var
159+
# (it clones main at runtime — gotcha #24) the flag is ignored, both arms run
160+
# the same path, and the comparison silently becomes A-vs-A. Demand the proof
161+
# line the windowed path prints.
162+
if windowed and "windows with" not in (r.stderr or ""):
163+
return None, "WINDOWED ARM DID NOT WINDOW — env var ignored by this binary: " + (r.stderr or "")[-400:]
153164
a = np.fromfile(dump, dtype=np.float32)
154165
return a.reshape(-1, 1024), None
155166

0 commit comments

Comments
 (0)