diff --git a/ventures/gandora/index.html b/ventures/gandora/index.html index e5806ec..fb42361 100644 --- a/ventures/gandora/index.html +++ b/ventures/gandora/index.html @@ -304,23 +304,33 @@ async function askDJ() { return spec; } -/* ACE-Step on Venice: queue, poll, download. 60 s costs about $0.03. */ +/* ACE-Step on Venice: queue, poll, download. 60 s costs about $0.03. Venice usually answers in 10-30 s but its queue + sometimes stalls a job for minutes, so after HEDGE_AT a second copy is queued and whichever lands first is taken. */ +const HEDGE_AT = 60000; async function renderTrack(spec, onStatus) { - const q = await venice("/audio/queue", { model: "ace-step-15", prompt: spec.tags, duration_seconds: 60, ...(spec.lyrics ? { lyrics_prompt: spec.lyrics } : {}) }); + const body = { model: "ace-step-15", prompt: spec.tags, duration_seconds: 60, ...(spec.lyrics ? { lyrics_prompt: spec.lyrics } : {}) }; + const jobs = [await venice("/audio/queue", body)]; const started = Date.now(); + let hedged = false; for (let i = 0; i < 90; i++) { await sleep(i < 4 ? 2500 : 4000); - const res = await venice("/audio/retrieve", { model: q.model || "ace-step-15", queue_id: q.queue_id, delete_media_on_completion: true }, { raw: true }); - const type = res.headers.get("content-type") || ""; - if (/^audio\//i.test(type) || /octet-stream/i.test(type)) { - const blob = await res.blob(); - state.cost += 0.03; localStorage.setItem(LS.cost, String(state.cost)); - return blob; + const elapsed = Date.now() - started; + if (!hedged && elapsed > HEDGE_AT) { hedged = true; try { jobs.push(await venice("/audio/queue", body)); } catch {} } + let expected = 0; + for (const q of jobs) { + const res = await venice("/audio/retrieve", { model: q.model || "ace-step-15", queue_id: q.queue_id, delete_media_on_completion: true }, { raw: true }); + const type = res.headers.get("content-type") || ""; + if (/^audio\//i.test(type) || /octet-stream/i.test(type)) { + const blob = await res.blob(); + state.cost += 0.03 * jobs.length; localStorage.setItem(LS.cost, String(state.cost)); + return blob; + } + const d = await res.json(); + const st = String(d.status || "").toUpperCase(); + if (/FAIL|ERROR|CANCEL|REJECT/.test(st)) { if (jobs.length === 1) throw new Error("Venice couldn't make the track (" + st.toLowerCase() + ")."); jobs.splice(jobs.indexOf(q), 1); break; } + expected = Math.max(expected, d.average_execution_time || 0); } - const d = await res.json(); - const st = String(d.status || "").toUpperCase(); - if (/FAIL|ERROR|CANCEL|REJECT/.test(st)) throw new Error("Venice couldn't make the track (" + st.toLowerCase() + ")."); - onStatus && onStatus(Math.round((Date.now() - started) / 1000), Math.round((d.average_execution_time || 0) / 1000)); + onStatus && onStatus(Math.round(elapsed / 1000), Math.round(expected / 1000), hedged); } throw new Error("That render took too long."); } @@ -337,8 +347,8 @@ let seq = 0; function makeJob(spec) { const job = { id: ++seq, spec, blob: null, err: null, done: null }; job.done = (async () => { - try { job.blob = await renderTrack(spec, (el, exp) => { - const t = `COOKING ${el}s${exp ? " / ~" + exp + "s" : ""}`; + try { job.blob = await renderTrack(spec, (el, exp, hedged) => { + const t = `COOKING ${el}s${exp ? " / ~" + exp + "s" : ""}${hedged ? " · SECOND OVEN LIT" : ""}`; if (state.current === job) $("#cover .veil").textContent = t; else if (state.nextJob === job && state.current) $("#next").textContent = "NEXT · " + spec.title.toUpperCase() + " · " + t; }); }