hetchyy Claude Haiku 4.5 commited on
Commit
35c5215
·
1 Parent(s): 7d8587f

Fix progress bar by replacing img onerror with MutationObserver elapsed counter

Browse files

The img onerror JS trick failed in Gradio's Svelte context because
getElementById couldn't find sibling elements at fire time. Replaced with
a data-ppb-duration attribute read by a MutationObserver installed at page
load, which starts a 0.1s interval timer showing "Xs / Ys". Also rounds
estimated duration down to nearest 5s instead of up.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

src/api/session_api.py CHANGED
@@ -272,7 +272,7 @@ def estimate_duration(endpoint, audio_duration_s=None, audio_id=None,
272
  if device == "CPU":
273
  estimate *= ESTIMATE_CPU_MULTIPLIER
274
 
275
- rounded = math.ceil(estimate / 5) * 5
276
 
277
  return {
278
  "endpoint": endpoint,
 
272
  if device == "CPU":
273
  estimate *= ESTIMATE_CPU_MULTIPLIER
274
 
275
+ rounded = max(5, math.floor(estimate / 5) * 5)
276
 
277
  return {
278
  "endpoint": endpoint,
src/ui/event_wiring.py CHANGED
@@ -1,10 +1,7 @@
1
  """Event wiring — connects all Gradio component events."""
2
  import gradio as gr
3
 
4
- from config import (
5
- DEV_TAB_VISIBLE,
6
- PROGRESS_PROCESS_AUDIO, PROGRESS_RESEGMENT, PROGRESS_RETRANSCRIBE,
7
- )
8
  from src.core.zero_gpu import QuotaExhaustedError
9
  from src.pipeline import (
10
  process_audio, resegment_audio,
@@ -99,7 +96,7 @@ def _wire_extract_chain(c):
99
  audio_dur = len(arr) / sr
100
  est = estimate_duration("process_audio_session", audio_dur, model_name=model, device=device)
101
  est_s = est.get("estimated_duration_s") or 30
102
- bar_html = pipeline_progress_bar_html(est_s, list(PROGRESS_PROCESS_AUDIO.values()))
103
 
104
  # Yield 1: show progress bar, hide extract button
105
  _skip = gr.update()
@@ -194,7 +191,7 @@ def _wire_resegment_chain(c):
194
  audio_dur = len(audio) / 16000 if audio is not None and hasattr(audio, '__len__') else None
195
  est = estimate_duration("resegment_session", audio_dur, model_name=model, device=device)
196
  est_s = est.get("estimated_duration_s") or 15
197
- bar_html = pipeline_progress_bar_html(est_s, list(PROGRESS_RESEGMENT.values()))
198
 
199
  _skip = gr.update()
200
  yield (
@@ -269,9 +266,7 @@ def _wire_retranscribe_chain(c):
269
  audio_dur = len(audio) / 16000 if audio is not None and hasattr(audio, '__len__') else None
270
  est = estimate_duration("retranscribe_session", audio_dur, model_name=model_name, device=device)
271
  est_s = est.get("estimated_duration_s") or 15
272
- opposite = "Large" if model_name == "Base" else "Base"
273
- stages = [(f, lbl.format(model=opposite)) for f, lbl in PROGRESS_RETRANSCRIBE.values()]
274
- bar_html = pipeline_progress_bar_html(est_s, stages)
275
 
276
  _skip = gr.update()
277
  yield (
 
1
  """Event wiring — connects all Gradio component events."""
2
  import gradio as gr
3
 
4
+ from config import DEV_TAB_VISIBLE
 
 
 
5
  from src.core.zero_gpu import QuotaExhaustedError
6
  from src.pipeline import (
7
  process_audio, resegment_audio,
 
96
  audio_dur = len(arr) / sr
97
  est = estimate_duration("process_audio_session", audio_dur, model_name=model, device=device)
98
  est_s = est.get("estimated_duration_s") or 30
99
+ bar_html = pipeline_progress_bar_html(est_s)
100
 
101
  # Yield 1: show progress bar, hide extract button
102
  _skip = gr.update()
 
191
  audio_dur = len(audio) / 16000 if audio is not None and hasattr(audio, '__len__') else None
192
  est = estimate_duration("resegment_session", audio_dur, model_name=model, device=device)
193
  est_s = est.get("estimated_duration_s") or 15
194
+ bar_html = pipeline_progress_bar_html(est_s)
195
 
196
  _skip = gr.update()
197
  yield (
 
266
  audio_dur = len(audio) / 16000 if audio is not None and hasattr(audio, '__len__') else None
267
  est = estimate_duration("retranscribe_session", audio_dur, model_name=model_name, device=device)
268
  est_s = est.get("estimated_duration_s") or 15
269
+ bar_html = pipeline_progress_bar_html(est_s)
 
 
270
 
271
  _skip = gr.update()
272
  yield (
src/ui/js_config.py CHANGED
@@ -38,6 +38,46 @@ def build_js_head(surah_ligatures: dict) -> str:
38
  core_js = (_STATIC_DIR / "animation-core.js").read_text(encoding="utf-8")
39
  all_js = (_STATIC_DIR / "animate-all.js").read_text(encoding="utf-8")
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  return (
42
  "<script>\n"
43
  f"{config}\n"
@@ -45,5 +85,6 @@ def build_js_head(surah_ligatures: dict) -> str:
45
  f"{core_js}\n"
46
  f"{all_js}\n"
47
  "})();\n"
 
48
  "</script>"
49
  )
 
38
  core_js = (_STATIC_DIR / "animation-core.js").read_text(encoding="utf-8")
39
  all_js = (_STATIC_DIR / "animate-all.js").read_text(encoding="utf-8")
40
 
41
+ ppb_observer_js = """
42
+ // Pipeline progress bar timer — fires when a bar is added to the DOM
43
+ (function() {
44
+ var _timers = {};
45
+ function _startPpbTimer(el) {
46
+ var uid = el.id;
47
+ if (!uid || _timers[uid]) return;
48
+ var dur = parseInt(el.getAttribute('data-ppb-duration'), 10) || 30;
49
+ var textEl = document.getElementById(uid + '-text');
50
+ if (!textEl) return;
51
+ var start = Date.now();
52
+ _timers[uid] = setInterval(function() {
53
+ if (!document.getElementById(uid)) {
54
+ clearInterval(_timers[uid]);
55
+ delete _timers[uid];
56
+ return;
57
+ }
58
+ var elapsed = (Date.now() - start) / 1000;
59
+ textEl.textContent = Math.floor(elapsed * 10) / 10 + 's / ' + dur + 's';
60
+ }, 100);
61
+ }
62
+ var _obs = new MutationObserver(function(mutations) {
63
+ for (var i = 0; i < mutations.length; i++) {
64
+ var added = mutations[i].addedNodes;
65
+ for (var j = 0; j < added.length; j++) {
66
+ var node = added[j];
67
+ if (node.nodeType !== 1) continue;
68
+ if (node.hasAttribute && node.hasAttribute('data-ppb-duration')) {
69
+ _startPpbTimer(node);
70
+ } else if (node.querySelectorAll) {
71
+ var found = node.querySelectorAll('[data-ppb-duration]');
72
+ for (var k = 0; k < found.length; k++) _startPpbTimer(found[k]);
73
+ }
74
+ }
75
+ }
76
+ });
77
+ _obs.observe(document.body, { childList: true, subtree: true });
78
+ })();
79
+ """
80
+
81
  return (
82
  "<script>\n"
83
  f"{config}\n"
 
85
  f"{core_js}\n"
86
  f"{all_js}\n"
87
  "})();\n"
88
+ f"{ppb_observer_js}\n"
89
  "</script>"
90
  )
src/ui/progress_bar.py CHANGED
@@ -1,42 +1,19 @@
1
- """Pipeline progress bar — CSS-animated fill with timed stage labels."""
2
 
3
- import json
4
  import random
5
 
6
 
7
- def pipeline_progress_bar_html(estimated_duration_s, stages):
8
  """Return HTML for a continuous progress bar timed to *estimated_duration_s*.
9
 
10
- *stages* is a list of ``(fraction, label)`` tuples taken from the
11
- ``PROGRESS_*`` config dicts. The label text changes at each fraction of
12
- the total duration via an ``img onerror`` JS trick (Gradio strips
13
- ``<script>`` tags from innerHTML updates).
14
  """
15
  uid = f"ppb{random.randint(0, 999999)}"
16
- duration = max(estimated_duration_s, 1)
17
 
18
- # Build JS array of [delay_ms, label] for the stage switcher
19
- stage_entries = []
20
- for frac, label in stages:
21
- delay_ms = int(frac * duration * 1000)
22
- stage_entries.append([delay_ms, label])
23
- stages_json = json.dumps(stage_entries)
24
-
25
- first_label = stages[0][1] if stages else "Processing..."
26
-
27
- counter_js = f'''<img src="data:," style="display:none"
28
- onerror="(function(){{
29
- var stages={stages_json},
30
- el=document.getElementById('{uid}-text');
31
- if(!el)return;
32
- for(var i=0;i<stages.length;i++){{
33
- (function(lbl,delay){{
34
- setTimeout(function(){{el.textContent=lbl;}},delay);
35
- }})(stages[i][1],stages[i][0]);
36
- }}
37
- }})()" />'''
38
-
39
- return f'''<div id="{uid}" style="
40
  position:relative; width:100%; height:40px;
41
  background:#e5e7eb; border-radius:8px; overflow:hidden;
42
  font-family:system-ui,sans-serif; font-size:14px;
@@ -52,12 +29,11 @@ def pipeline_progress_bar_html(estimated_duration_s, stages):
52
  align-items:center; justify-content:center;
53
  color:#1f2937; font-weight:600; z-index:1;
54
  text-shadow:0 0 4px rgba(255,255,255,0.8);
55
- ">{first_label}</span>
56
  <style>
57
  @keyframes {uid}-grow {{
58
  from {{ width:0%; }}
59
  to {{ width:100%; }}
60
  }}
61
  </style>
62
- {counter_js}
63
  </div>'''
 
1
+ """Pipeline progress bar — CSS-animated fill with elapsed counter."""
2
 
 
3
  import random
4
 
5
 
6
+ def pipeline_progress_bar_html(estimated_duration_s):
7
  """Return HTML for a continuous progress bar timed to *estimated_duration_s*.
8
 
9
+ The container ``<div>`` carries a ``data-ppb-duration`` attribute that a
10
+ MutationObserver in the page head reads to start a 0.1 s elapsed-time
11
+ counter (``Xs / Ys``).
 
12
  """
13
  uid = f"ppb{random.randint(0, 999999)}"
14
+ duration = max(estimated_duration_s, 5)
15
 
16
+ return f'''<div id="{uid}" data-ppb-duration="{duration}" style="
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  position:relative; width:100%; height:40px;
18
  background:#e5e7eb; border-radius:8px; overflow:hidden;
19
  font-family:system-ui,sans-serif; font-size:14px;
 
29
  align-items:center; justify-content:center;
30
  color:#1f2937; font-weight:600; z-index:1;
31
  text-shadow:0 0 4px rgba(255,255,255,0.8);
32
+ ">0s / {duration}s</span>
33
  <style>
34
  @keyframes {uid}-grow {{
35
  from {{ width:0%; }}
36
  to {{ width:100%; }}
37
  }}
38
  </style>
 
39
  </div>'''