harness needs no audio file: it renders a generated wav; README wording
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
1 parent
4a33a30820
commit
a54bfdff7e
3 files changed
+19
-22
No files matched your search
@@ -12,9 +12,10 @@ It is one static page. Serve the folder and open it:
|
|||||||
|
|
||||||
python3 -m http.server 8794 && open http://127.0.0.1:8794/
|
python3 -m http.server 8794 && open http://127.0.0.1:8794/
|
||||||
|
|
||||||
It talks to the factory's inference worker at `https://inference.yolopreneur.lol` (allowed origins include
|
It talks to the factory's inference worker at `https://inference.yolopreneur.lol`, which only answers pages served
|
||||||
localhost? no: only `*.yolopreneur.lol` and `gandora.lol`; for local work add your Venice key with `/?key` or run
|
from `*.yolopreneur.lol` or `gandora.lol`. For local work either add a personal Venice key (open `/?key` to show the
|
||||||
from a `*.yolopreneur.lol` host). Everything the listener makes lives in their browser (IndexedDB) — see STORAGE.md.
|
key box) or test with `tools/harness.sh`, which stubs the backend. Everything the listener makes lives in their
|
||||||
|
browser (IndexedDB) — see STORAGE.md.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ from a `*.yolopreneur.lol` host). Everything the listener makes lives in their b
|
|||||||
- `sw.js` — service worker: caches the shell; stamped with the commit by the shots workflow.
|
- `sw.js` — service worker: caches the shell; stamped with the commit by the shots workflow.
|
||||||
- `manifest.json`, `icons/`, `favicon.svg` — PWA. Icons are drawn by `tools/icons.py` (stdlib only).
|
- `manifest.json`, `icons/`, `favicon.svg` — PWA. Icons are drawn by `tools/icons.py` (stdlib only).
|
||||||
- `tools/shots.sh` — the card image and the icons, run by `.gitea/workflows/shots.yml` on every push.
|
- `tools/shots.sh` — the card image and the icons, run by `.gitea/workflows/shots.yml` on every push.
|
||||||
- `tools/harness.html` — the playback regression harness (see ARCHITECTURE.md › Testing).
|
- `tools/harness.sh` + `tools/harness.html` — the playback regression harness (ARCHITECTURE.md › Testing).
|
||||||
- `STORAGE.md` — the IndexedDB contract. `ARCHITECTURE.md` — how it all fits. `HANDOFF.md` — read before changing things.
|
- `STORAGE.md` — the IndexedDB contract. `ARCHITECTURE.md` — how it all fits. `HANDOFF.md` — read before changing things.
|
||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
|
|||||||
+11
-13
@@ -5,32 +5,30 @@
|
|||||||
<!-- harness -->
|
<!-- harness -->
|
||||||
<script>
|
<script>
|
||||||
try {
|
try {
|
||||||
const MP3 = "data:audio/mpeg;base64,__MP3__";
|
|
||||||
async function mp3blob() { const b = atob(MP3.split(",")[1]); const u = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) u[i] = b.charCodeAt(i); return new Blob([u], { type: "audio/mpeg" }); }
|
|
||||||
let renderDelay = 50, n = 0;
|
let renderDelay = 50, n = 0;
|
||||||
venice = async function (path, body, { raw = false } = {}) {
|
venice = async function (path, body, { raw = false } = {}) {
|
||||||
if (path.includes("chat")) return { choices: [{ message: { content: JSON.stringify({ title: "T" + (++n), tags: "lo-fi, piano, instrumental", lyrics: "[verse]\nla\n[chorus]\nlo", bpm: 80, key: "C major", energy: 0.4, seconds: 30, meter: "4/4", station: "Test FM", line: "hi" }) } }] };
|
if (path.includes("chat")) return { choices: [{ message: { content: JSON.stringify({ title: "T" + (++n), tags: "lo-fi, piano, instrumental", lyrics: "[verse]\nla\n[chorus]\nlo", bpm: 80, key: "C major", energy: 0.4, seconds: 30, meter: "4/4", station: "Test FM", line: "hi" }) } }] };
|
||||||
if (path === "/render") { await new Promise((r) => setTimeout(r, renderDelay)); const b = await mp3blob(); return new Response(b, { headers: { "content-type": "audio/mpeg", "x-cost": "0.001", "x-renderer": "test" } }); }
|
if (path === "/render") { await new Promise((r) => setTimeout(r, renderDelay)); const b = wavBlob(new Int16Array(8000 * 2), 8000); return new Response(b, { headers: { "content-type": "audio/wav", "x-cost": "0.001", "x-renderer": "test" } }); }
|
||||||
throw new Error("unexpected " + path);
|
throw new Error("unexpected " + path);
|
||||||
};
|
};
|
||||||
const log = []; const dd = document.createElement("pre"); dd.id = "diag"; document.body.append(dd);
|
const log = []; const dd = document.createElement("pre"); dd.id = "diag"; document.body.append(dd);
|
||||||
const L = (m) => { log.push(m); dd.textContent = log.join("\n"); };
|
const L = (m) => { log.push(m); dd.textContent = log.join("\n"); };
|
||||||
const S = (tag) => L(tag + ": src=" + String(state.audio.src).slice(5, 14) + " paused=" + state.audio.paused + " playing=" + state.playing + " holding=" + !!state.holding + " cur=" + (state.current && state.current.spec.title) + " next=" + (state.nextJob && state.nextJob.spec.title) + " nextblob=" + !!(state.nextJob && state.nextJob.blob) + " crackle=" + crackle.on + " loop=" + state.audio.loop);
|
const S = (tag) => L(tag + "\n src=" + String(state.audio.src).slice(5, 14) + " paused=" + state.audio.paused + " playing=" + state.playing + " holding=" + !!state.holding + " cur=" + (state.current && state.current.spec.title) + " next=" + (state.nextJob && state.nextJob.spec.title) + " nextblob=" + !!(state.nextJob && state.nextJob.blob) + " crackle=" + crackle.on + " loop=" + state.audio.loop);
|
||||||
window.addEventListener("error", (e) => L("ERR " + e.message + " @" + e.lineno));
|
window.addEventListener("error", (e) => L("ERR " + e.message + " @" + e.lineno));
|
||||||
window.addEventListener("unhandledrejection", (e) => L("REJ " + (e.reason && (e.reason.stack || e.reason)).toString().slice(0, 200)));
|
window.addEventListener("unhandledrejection", (e) => L("REJ " + (e.reason && (e.reason.stack || e.reason)).toString().slice(0, 200)));
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
const ended = () => { state.audio.dispatchEvent(new Event("ended")); };
|
const ended = () => { state.audio.dispatchEvent(new Event("ended")); };
|
||||||
(async () => {
|
(async () => {
|
||||||
document.dispatchEvent(new Event("keydown")); await sleep(300);
|
document.dispatchEvent(new Event("keydown")); await sleep(300);
|
||||||
openStation("test seed"); await sleep(1500); S("after first (expect cur=T1 playing, next=T2 cooked)");
|
openStation("test seed"); await sleep(1500); S("after first expect cur=T1 playing, next=T2 nextblob=true");
|
||||||
ended(); await sleep(600); S("after ended (expect cur=T2 playing, holding=false)");
|
ended(); await sleep(600); S("after ended expect cur=T2 playing, holding=false");
|
||||||
renderDelay = 3000; state.nextJob = null; cookNext(); await sleep(300); S("slow cook started (expect next cooking, nextblob=false)");
|
renderDelay = 3000; state.nextJob = null; cookNext(); await sleep(300); S("slow cook started expect next cooking, nextblob=false");
|
||||||
ended(); await sleep(600); S("ended, nothing ready (expect holding=true, loop=true)");
|
ended(); await sleep(600); S("ended, nothing ready expect holding=true loop=true");
|
||||||
await sleep(3500); S("track landed (expect holding=true, loop=false: queued behind the crackle)");
|
await sleep(3500); S("track landed expect holding=true loop=false (queued behind the crackle)");
|
||||||
ended(); await sleep(600); S("crackle pass ended (expect cur=next track playing, holding=false)");
|
ended(); await sleep(600); S("crackle pass ended expect cur=that track playing, holding=false");
|
||||||
renderDelay = 50; document.querySelector("#play").click(); await sleep(600); S("after pause (expect paused, crackle=true)");
|
renderDelay = 50; document.querySelector("#play").click(); await sleep(600); S("after pause expect paused=true crackle=true");
|
||||||
document.querySelector("#play").click(); await sleep(300); S("after resume (expect playing, crackle=false)");
|
document.querySelector("#play").click(); await sleep(300); S("after resume expect paused=false crackle=false");
|
||||||
document.querySelector("#skip").click(); await sleep(1500); S("after skip (expect a new cur)");
|
document.querySelector("#skip").click(); await sleep(1500); S("after skip expect a new cur");
|
||||||
L("END");
|
L("END");
|
||||||
})();
|
})();
|
||||||
} catch (e) { const p = document.createElement("pre"); p.id = "diag"; p.textContent = "TOPERR " + e.message; document.body.append(p); }
|
} catch (e) { const p = document.createElement("pre"); p.id = "diag"; p.textContent = "TOPERR " + e.message; document.body.append(p); }
|
||||||
|
|||||||
+3
-5
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# tools/harness.sh: the playback regression run. Builds tools/harness.html's page from index.html, runs it in headless
|
# tools/harness.sh: the playback regression run. Appends tools/harness.html's script to index.html, runs it in headless
|
||||||
# Chrome with autoplay allowed, prints one state line per step. Needs Chrome/Chromium and python3.
|
# Chrome with autoplay allowed, prints one state line per step. Needs Chrome/Chromium and python3.
|
||||||
set -eu
|
set -eu
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
@@ -15,12 +15,10 @@ out = pathlib.Path(sys.argv[1])
|
|||||||
page = pathlib.Path("index.html").read_text()
|
page = pathlib.Path("index.html").read_text()
|
||||||
h = pathlib.Path("tools/harness.html").read_text()
|
h = pathlib.Path("tools/harness.html").read_text()
|
||||||
script = h[h.index("<!-- harness -->"):]
|
script = h[h.index("<!-- harness -->"):]
|
||||||
mp3 = pathlib.Path("tools/harness.mp3").read_bytes() if pathlib.Path("tools/harness.mp3").exists() else b""
|
|
||||||
import base64
|
|
||||||
script = script.replace("__MP3__", base64.b64encode(mp3).decode())
|
|
||||||
(out / "run.html").write_text(page.replace("</body>", script + "</body>"))
|
(out / "run.html").write_text(page.replace("</body>", script + "</body>"))
|
||||||
EOF
|
EOF
|
||||||
"$CHROME" --headless=new --disable-gpu --autoplay-policy=no-user-gesture-required --virtual-time-budget=25000 --dump-dom "file://$out/run.html" 2>/dev/null > "$out/dom.html" || true
|
# shellcheck disable=SC2086
|
||||||
|
"$CHROME" --headless=new --disable-gpu --autoplay-policy=no-user-gesture-required --virtual-time-budget=25000 ${CHROME_FLAGS:-} --dump-dom "file://$out/run.html" 2>/dev/null > "$out/dom.html" || true
|
||||||
python3 - "$out/dom.html" <<'EOF'
|
python3 - "$out/dom.html" <<'EOF'
|
||||||
import sys, re, html
|
import sys, re, html
|
||||||
s = open(sys.argv[1]).read()
|
s = open(sys.argv[1]).read()
|
||||||
|
|||||||
Reference in new issue
Block a user