Spaces:
Running on Zero
Running on Zero
Fix progress bar by replacing img onerror with MutationObserver elapsed counter
Browse filesThe 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 +1 -1
- src/ui/event_wiring.py +4 -9
- src/ui/js_config.py +41 -0
- src/ui/progress_bar.py +8 -32
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.
|
| 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
|
| 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
|
| 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 |
-
|
| 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
|
| 2 |
|
| 3 |
-
import json
|
| 4 |
import random
|
| 5 |
|
| 6 |
|
| 7 |
-
def pipeline_progress_bar_html(estimated_duration_s
|
| 8 |
"""Return HTML for a continuous progress bar timed to *estimated_duration_s*.
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
``<script>`` tags from innerHTML updates).
|
| 14 |
"""
|
| 15 |
uid = f"ppb{random.randint(0, 999999)}"
|
| 16 |
-
duration = max(estimated_duration_s,
|
| 17 |
|
| 18 |
-
|
| 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 |
-
">{
|
| 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>'''
|