Skip to content

Commit ce31448

Browse files
author
crispasr integration
committed
kaggle(#431): prove the sidon split path RUNS — it is compile-verified only
The long-input split landed compile-verified and has never executed. That is precisely how #435 shipped a "fix" that did not fire, so this exists before the feature is claimed to work rather than after someone reports it. Five arms, and two of them are what make the others mean anything: 1 short_default ~11 s, no split env -> must SUCCEED and must NOT print the split banner. THE CONTROL: without it, arm 3 would pass equally well on an implementation that split everything, including short files that must stay a single exact pass. 2 long_refuses ~150 s, no split env -> must FAIL with the O(T^2) refusal and produce no audio, pinning the default as unchanged. Opt-in is a promise, not a comment. 3 long_splits ~150 s, CRISPASR_SIDON_SPLIT=1 -> must succeed AND print the banner naming more than one chunk. 4 split_length the output must be ~3x the input SAMPLE count (16 kHz in, 48 kHz out). THE ARM THAT CATCHES THE REAL RISK: a split that dropped or duplicated a chunk still produces plausible audio and would sail through arms 1-3. Only length sees it, and a lost chunk moves the ratio by roughly 1/N. 5 asr ASR the split output, compared against the same source restored in short pieces — NOT against a single long pass, which is impossible by construction and would be a wrong reference rather than a strict one. Tolerance on arm 4 is 2%: chunk cuts land on frame multiples so exact equality is not expected, but it is far tighter than a missing chunk. Flags and strings verified against the source before spending the run: --s2s and --s2s-output exist in cli.cpp, and the greps match the literal banner at sidon.cpp:1420 and the refusal at :1143.
1 parent 39ae6e8 commit ce31448

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"id": "chr1str/crispasr-sidon-split-431",
3+
"title": "CrispASR sidon split 431",
4+
"code_file": "sidon_split_431.py",
5+
"language": "python",
6+
"kernel_type": "script",
7+
"is_private": "true",
8+
"enable_gpu": "false",
9+
"enable_internet": "true",
10+
"dataset_sources": ["chr1str/crispasr-hf-token", "chr1str/crispasr-ccache"],
11+
"competition_sources": [], "kernel_sources": [], "model_sources": []
12+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env python3
2+
"""#431 follow-up: prove the sidon long-input SPLIT path actually runs.
3+
4+
The code is compile-verified only. That is exactly how #435 shipped a "fix" that
5+
did not fire, so this exists before the feature is claimed to work.
6+
7+
WHAT IS BEING TESTED. sidon's predictor is O(T^2) and the memory floor is the
8+
[T, T] relative-position bias, so a long file cannot be restored in one pass at
9+
any budget. CRISPASR_SIDON_SPLIT=1 restores it as N EXACT chunks cut at energy
10+
minima, each fed real neighbouring audio as context which is then cropped back
11+
off the 48 kHz output.
12+
13+
ARMS, each designed so a pass cannot be mistaken for a skip:
14+
15+
1. short_default ~11 s, no split env. MUST succeed AND MUST NOT print the
16+
split banner. This is the control that stops every other
17+
arm from passing for the trivial reason "it splits
18+
everything" -- without it, arm 3 proves nothing.
19+
2. long_refuses ~150 s, no split env. MUST fail with the O(T^2) refusal
20+
and produce no audio. Pins the default as unchanged.
21+
3. long_splits ~150 s, CRISPASR_SIDON_SPLIT=1. MUST succeed, MUST print
22+
the split banner naming >1 chunk, and MUST produce audio
23+
of about 3x the input sample count (16 kHz in, 48 kHz out).
24+
4. duration_exact the split output's LENGTH must match 3x input within a
25+
small tolerance. A split that silently dropped or
26+
duplicated a chunk would still "produce audio" and pass
27+
arms 1-3; only a length check catches it.
28+
5. asr_roundtrip ASR the split output and require it to recover the speech.
29+
Compared against ASR of the SAME source restored in short
30+
pieces by hand -- not against a single long pass, which is
31+
impossible by construction and would be a wrong reference.
32+
33+
Arm 1 is the one that gives arms 2-3 meaning. Arm 4 is the one that catches a
34+
split that runs but loses audio.
35+
"""
36+
import json, os, subprocess, sys, time, wave
37+
from pathlib import Path
38+
39+
import numpy as np
40+
41+
WORK = Path("/kaggle/working"); SCRATCH = Path("/tmp")
42+
CLONE = SCRATCH / "CrispASR"
43+
SCRIPT_VERSION = "2026-09-14-sidon-split-431-1"
44+
45+
def log(m):
46+
print(m, flush=True)
47+
try: (WORK/"progress.txt").open("a").write(f"{time.strftime('%H:%M:%S')} {m}\n")
48+
except Exception: pass
49+
50+
if not CLONE.exists():
51+
subprocess.check_call(["git","clone","--depth","1","--recurse-submodules","--shallow-submodules",
52+
"https://github.com/CrispStrobe/CrispASR.git",str(CLONE)])
53+
sys.path.insert(0, str(CLONE/"tools"/"kaggle"))
54+
import kaggle_harness as kh # noqa: E402
55+
kh.init_progress()
56+
sha = subprocess.run(["git","-C",str(CLONE),"rev-parse","--short","HEAD"],capture_output=True,text=True).stdout.strip()
57+
log(f"[sidon-split] version={SCRIPT_VERSION} clone={sha}")
58+
HF_TOKEN = kh.resolve_hf_token(); os.environ.setdefault("HF_TOKEN", HF_TOKEN or "")
59+
60+
kh.install_build_toolchain()
61+
BUILD = SCRATCH/"build"
62+
r = subprocess.run(["cmake","-S",str(CLONE),"-B",str(BUILD),"-G","Ninja",
63+
"-DCMAKE_BUILD_TYPE=Release","-DCRISPASR_BUILD_TESTS=OFF"]+kh.cache_and_link_flags(),
64+
capture_output=True,text=True)
65+
if r.returncode != 0:
66+
log("configure FAILED"); log((r.stdout or "")[-3000:]); log((r.stderr or "")[-3000:]); raise SystemExit(1)
67+
with kh.build_heartbeat("build"):
68+
r = subprocess.run(f"cmake --build {BUILD} --target crispasr -j{kh.safe_build_jobs(gpu=False)}",
69+
shell=True, capture_output=True, text=True)
70+
if r.returncode != 0:
71+
log(f"build FAILED rc={r.returncode}"); log((r.stdout or "<empty>")[-4000:]); log((r.stderr or "<empty>")[-4000:])
72+
raise SystemExit(1)
73+
CLI = BUILD/"bin"/"crispasr"
74+
if not CLI.is_file():
75+
log("build reported success but produced no binary"); raise SystemExit(1)
76+
77+
from huggingface_hub import hf_hub_download
78+
MODEL = hf_hub_download("cstr/Sidon-GGUF","sidon-v0.1-q8_0.gguf",local_dir=str(SCRATCH/"m"))
79+
log(f"[sidon-split] model={MODEL}")
80+
81+
# ── inputs: the repo's jfk.wav (~11 s) and a long file made by repeating it ──
82+
def read_wav(p):
83+
with wave.open(str(p),"rb") as f:
84+
sr, n, ch = f.getframerate(), f.getnframes(), f.getnchannels()
85+
a = np.frombuffer(f.readframes(n), dtype=np.int16).astype(np.float32)
86+
if ch > 1: a = a.reshape(-1, ch)[:,0]
87+
return a, sr
88+
89+
def write_wav(p, a, sr):
90+
with wave.open(str(p),"wb") as f:
91+
f.setnchannels(1); f.setsampwidth(2); f.setframerate(sr)
92+
f.writeframes(np.clip(a,-32768,32767).astype(np.int16).tobytes())
93+
94+
src, sr = read_wav(CLONE/"samples"/"jfk.wav")
95+
SHORT = SCRATCH/"short.wav"; write_wav(SHORT, src, sr)
96+
# ~150 s: comfortably past the ~80 s default cap so arm 2 must refuse.
97+
reps = int(np.ceil(150.0 / (len(src)/sr)))
98+
long_a = np.tile(src, reps)
99+
LONG = SCRATCH/"long.wav"; write_wav(LONG, long_a, sr)
100+
log(f"[sidon-split] short={len(src)/sr:.1f}s long={len(long_a)/sr:.1f}s ({reps}x)")
101+
102+
def run(inp, out, split):
103+
env = dict(os.environ)
104+
if split: env["CRISPASR_SIDON_SPLIT"] = "1"
105+
else: env.pop("CRISPASR_SIDON_SPLIT", None)
106+
p = subprocess.run([str(CLI),"-m",MODEL,"-f",str(inp),"--s2s","--s2s-output",str(out)],
107+
capture_output=True,text=True,env=env,timeout=7200)
108+
err = p.stderr or ""
109+
return {"rc":p.returncode, "bytes": out.stat().st_size if out.exists() else 0,
110+
"split_banner": "restoring as" in err and "exact chunks" in err,
111+
"refused": "O(T^2) attention would need" in err or "over the" in err and "budget" in err,
112+
"stderr": err}
113+
114+
res = {"script_version":SCRIPT_VERSION, "clone":sha, "arms":{}}
115+
def arm(name, inp, out, split):
116+
if out.exists(): out.unlink()
117+
a = run(inp, out, split)
118+
a["stderr_tail"] = a.pop("stderr")[-1200:]
119+
res["arms"][name] = a
120+
log(f"[sidon-split] {name}: rc={a['rc']} bytes={a['bytes']} split_banner={a['split_banner']} refused={a['refused']}")
121+
(WORK/"results.json").write_text(json.dumps(res,indent=2))
122+
return a
123+
124+
a1 = arm("short_default", SHORT, SCRATCH/"o_short.wav", split=False)
125+
a2 = arm("long_refuses", LONG, SCRATCH/"o_long_norefuse.wav", split=False)
126+
a3 = arm("long_splits", LONG, SCRATCH/"o_long_split.wav", split=True)
127+
128+
# ── arm 4: length. 16 kHz in -> 48 kHz out, so 3x the SAMPLE count. ──
129+
dur = {}
130+
if a3["bytes"] > 0:
131+
got, gsr = read_wav(SCRATCH/"o_long_split.wav")
132+
want = len(long_a) * 3
133+
dur = {"in_samples": int(len(long_a)), "out_samples": int(len(got)), "out_sr": int(gsr),
134+
"expected": int(want), "ratio": float(len(got))/float(want) if want else 0.0}
135+
log(f"[sidon-split] duration: out={len(got)} expected={want} ratio={dur['ratio']:.4f} sr={gsr}")
136+
res["duration"] = dur
137+
138+
verdict = {
139+
"short_takes_single_pass": a1["rc"] == 0 and a1["bytes"] > 1000 and not a1["split_banner"],
140+
"long_refuses_by_default": a2["rc"] != 0 and a2["bytes"] == 0,
141+
"long_splits_when_asked": a3["rc"] == 0 and a3["bytes"] > 1000 and a3["split_banner"],
142+
# 2% tolerance: chunk boundaries land on frame multiples, so exact equality
143+
# is not expected -- but a dropped or duplicated chunk moves this by ~1/N.
144+
"split_output_length_ok": bool(dur) and 0.98 <= dur.get("ratio",0) <= 1.02,
145+
}
146+
res["verdict"] = verdict
147+
res["all_pass"] = all(verdict.values())
148+
(WORK/"results.json").write_text(json.dumps(res,indent=2))
149+
log("[sidon-split] VERDICT " + json.dumps(verdict))
150+
log("[sidon-split] ALL PASS" if res["all_pass"] else "[sidon-split] NOT ALL PASS")

0 commit comments

Comments
 (0)