ACloudCenter Claude Fable 5 commited on
Commit
211e07b
·
1 Parent(s): b09db11

Interpolate progress inside the current wave

Browse files

Counting 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>

Files changed (2) hide show
  1. static/app.js +44 -9
  2. 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 = { startedAt: 0, wave: 0, totalWaves: 0, ticker: null, label: "" };
 
 
 
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
- const frac = done / progress.totalWaves;
1587
- const width = `${Math.max(2, frac * 100).toFixed(1)}%`;
 
 
 
 
 
 
 
 
 
 
 
1588
  el.progressTrack.hidden = false;
1589
- el.progressFill.style.width = width;
1590
  el.genStageTrack.hidden = false;
1591
- el.genStageFill.style.width = width;
1592
- pctText = `${Math.round(frac * 100)}%`;
 
 
 
 
 
 
 
 
 
 
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
- progress.wave = Number(m[1]);
 
 
 
 
 
 
 
 
 
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;