Commit ·
211e07b
1
Parent(s): b09db11
Interpolate progress inside the current wave
Browse filesCounting only completed waves meant the percentage sat at 0% for the
whole of wave 1 — minutes on a long render — then jumped. Now, once a
wave has finished we know roughly how long one takes, so progress
interpolates within the wave in flight and the ETA uses the same
fraction. Before that first data point there is no honest number, so
the bar sweeps indeterminate and reads 'Rendering…' rather than faking
a zero.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- static/app.js +44 -9
- static/styles.css +10 -0
static/app.js
CHANGED
|
@@ -1567,7 +1567,10 @@ el.stopGenBtn.addEventListener("click", () => {
|
|
| 1567 |
useless on a 70-minute render. Its status line carries the real signal
|
| 1568 |
("Rendering wave 3/21 …"), so drive progress off completed waves and
|
| 1569 |
keep a client-side clock ticking between events. */
|
| 1570 |
-
const progress = {
|
|
|
|
|
|
|
|
|
|
| 1571 |
|
| 1572 |
function formatMinutes(seconds) {
|
| 1573 |
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
@@ -1583,20 +1586,39 @@ function paintProgress() {
|
|
| 1583 |
let pctText = "Starting…";
|
| 1584 |
if (progress.totalWaves) {
|
| 1585 |
const done = Math.max(0, progress.wave - 1);
|
| 1586 |
-
|
| 1587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1588 |
el.progressTrack.hidden = false;
|
| 1589 |
-
el.progressFill.style.width = width;
|
| 1590 |
el.genStageTrack.hidden = false;
|
| 1591 |
-
el.
|
| 1592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1593 |
parts.push(`Wave ${progress.wave} of ${progress.totalWaves}`);
|
| 1594 |
if (done >= 1) {
|
| 1595 |
-
const remaining = (elapsed / done) * (progress.totalWaves - done);
|
| 1596 |
const eta = formatMinutes(remaining);
|
| 1597 |
if (eta) parts.push(`~${eta} left`);
|
| 1598 |
}
|
| 1599 |
-
document.title = `${pctText} · Chorus`;
|
| 1600 |
} else if (progress.label) {
|
| 1601 |
parts.push(progress.label);
|
| 1602 |
}
|
|
@@ -1633,7 +1655,11 @@ function startProgress() {
|
|
| 1633 |
progress.wave = 0;
|
| 1634 |
progress.totalWaves = 0;
|
| 1635 |
progress.label = "";
|
|
|
|
|
|
|
| 1636 |
el.progressFill.style.width = "0%";
|
|
|
|
|
|
|
| 1637 |
el.progressTrack.hidden = true;
|
| 1638 |
clearInterval(progress.ticker);
|
| 1639 |
progress.ticker = setInterval(paintProgress, 1000);
|
|
@@ -1653,7 +1679,16 @@ function noteProgressFromStatus(text) {
|
|
| 1653 |
if (!text) return;
|
| 1654 |
const m = text.match(/wave\s+(\d+)\s*\/\s*(\d+)/i);
|
| 1655 |
if (m) {
|
| 1656 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1657 |
progress.totalWaves = Number(m[2]);
|
| 1658 |
} else if (/re-?rolling/i.test(text)) {
|
| 1659 |
progress.label = "Re-rolling flagged chunks";
|
|
|
|
| 1567 |
useless on a 70-minute render. Its status line carries the real signal
|
| 1568 |
("Rendering wave 3/21 …"), so drive progress off completed waves and
|
| 1569 |
keep a client-side clock ticking between events. */
|
| 1570 |
+
const progress = {
|
| 1571 |
+
startedAt: 0, wave: 0, totalWaves: 0, ticker: null, label: "",
|
| 1572 |
+
waveStartedAt: 0, waveDurations: [],
|
| 1573 |
+
};
|
| 1574 |
|
| 1575 |
function formatMinutes(seconds) {
|
| 1576 |
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
|
|
| 1586 |
let pctText = "Starting…";
|
| 1587 |
if (progress.totalWaves) {
|
| 1588 |
const done = Math.max(0, progress.wave - 1);
|
| 1589 |
+
// Waves land in steps minutes apart, so a bare completed-wave count would
|
| 1590 |
+
// sit at 0% through all of wave 1 and then jump. Once a wave has finished
|
| 1591 |
+
// we know roughly how long one takes, so interpolate inside the current
|
| 1592 |
+
// one; until then the bar runs indeterminate rather than faking a number.
|
| 1593 |
+
const avgWave = progress.waveDurations.length
|
| 1594 |
+
? progress.waveDurations.reduce((a, b) => a + b, 0) / progress.waveDurations.length
|
| 1595 |
+
: 0;
|
| 1596 |
+
const inWave = avgWave
|
| 1597 |
+
? Math.min(0.98, ((Date.now() - progress.waveStartedAt) / 1000) / avgWave)
|
| 1598 |
+
: 0;
|
| 1599 |
+
const indeterminate = !avgWave;
|
| 1600 |
+
const frac = (done + inWave) / progress.totalWaves;
|
| 1601 |
+
|
| 1602 |
el.progressTrack.hidden = false;
|
|
|
|
| 1603 |
el.genStageTrack.hidden = false;
|
| 1604 |
+
el.progressTrack.classList.toggle("indeterminate", indeterminate);
|
| 1605 |
+
el.genStageTrack.classList.toggle("indeterminate", indeterminate);
|
| 1606 |
+
if (!indeterminate) {
|
| 1607 |
+
const width = `${Math.max(1.5, frac * 100).toFixed(1)}%`;
|
| 1608 |
+
el.progressFill.style.width = width;
|
| 1609 |
+
el.genStageFill.style.width = width;
|
| 1610 |
+
pctText = `${Math.round(frac * 100)}%`;
|
| 1611 |
+
document.title = `${pctText} · Chorus`;
|
| 1612 |
+
} else {
|
| 1613 |
+
pctText = "Rendering…";
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
parts.push(`Wave ${progress.wave} of ${progress.totalWaves}`);
|
| 1617 |
if (done >= 1) {
|
| 1618 |
+
const remaining = (elapsed / (done + inWave)) * (progress.totalWaves - done - inWave);
|
| 1619 |
const eta = formatMinutes(remaining);
|
| 1620 |
if (eta) parts.push(`~${eta} left`);
|
| 1621 |
}
|
|
|
|
| 1622 |
} else if (progress.label) {
|
| 1623 |
parts.push(progress.label);
|
| 1624 |
}
|
|
|
|
| 1655 |
progress.wave = 0;
|
| 1656 |
progress.totalWaves = 0;
|
| 1657 |
progress.label = "";
|
| 1658 |
+
progress.waveStartedAt = 0;
|
| 1659 |
+
progress.waveDurations = [];
|
| 1660 |
el.progressFill.style.width = "0%";
|
| 1661 |
+
el.progressTrack.classList.remove("indeterminate");
|
| 1662 |
+
el.genStageTrack.classList.remove("indeterminate");
|
| 1663 |
el.progressTrack.hidden = true;
|
| 1664 |
clearInterval(progress.ticker);
|
| 1665 |
progress.ticker = setInterval(paintProgress, 1000);
|
|
|
|
| 1679 |
if (!text) return;
|
| 1680 |
const m = text.match(/wave\s+(\d+)\s*\/\s*(\d+)/i);
|
| 1681 |
if (m) {
|
| 1682 |
+
const wave = Number(m[1]);
|
| 1683 |
+
if (wave !== progress.wave) {
|
| 1684 |
+
// A new wave means the previous one just finished — time it, so the
|
| 1685 |
+
// in-wave interpolation and the ETA have something real to work from.
|
| 1686 |
+
if (progress.wave && progress.waveStartedAt) {
|
| 1687 |
+
progress.waveDurations.push((Date.now() - progress.waveStartedAt) / 1000);
|
| 1688 |
+
}
|
| 1689 |
+
progress.waveStartedAt = Date.now();
|
| 1690 |
+
progress.wave = wave;
|
| 1691 |
+
}
|
| 1692 |
progress.totalWaves = Number(m[2]);
|
| 1693 |
} else if (/re-?rolling/i.test(text)) {
|
| 1694 |
progress.label = "Re-rolling flagged chunks";
|
static/styles.css
CHANGED
|
@@ -660,6 +660,16 @@ select:focus { outline: none; border-color: #d9a98c; }
|
|
| 660 |
background: var(--accent);
|
| 661 |
transition: width 0.4s ease;
|
| 662 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
.progress-meta {
|
| 664 |
font-size: 0.74rem;
|
| 665 |
font-weight: 600;
|
|
|
|
| 660 |
background: var(--accent);
|
| 661 |
transition: width 0.4s ease;
|
| 662 |
}
|
| 663 |
+
/* Before any wave has finished there is no honest number to show, so sweep. */
|
| 664 |
+
.progress-track.indeterminate .progress-fill {
|
| 665 |
+
width: 32%;
|
| 666 |
+
transition: none;
|
| 667 |
+
animation: indeterminateSweep 1.7s ease-in-out infinite;
|
| 668 |
+
}
|
| 669 |
+
@keyframes indeterminateSweep {
|
| 670 |
+
0% { transform: translateX(-110%); }
|
| 671 |
+
100% { transform: translateX(320%); }
|
| 672 |
+
}
|
| 673 |
.progress-meta {
|
| 674 |
font-size: 0.74rem;
|
| 675 |
font-weight: 600;
|