fix: 4 Space runtime bugs found in line-by-line audit
Browse files1. streaming=True on gr.Image webcam meant button-click handlers got
None as the frame value β root cause of 'no webcam frame yet'.
Switched to streaming=False; user still sees live preview, and the
Capture click reliably delivers the current frame.
2. gradio 4.44.1 imports HfFolder from huggingface_hub, which is
removed in 0.30+. Pinned huggingface_hub>=0.26,<0.30.
3. Added text labels above the webcam (".signbridge-webcam-help" HTML
blocks) explaining Start / Stop / Capture flow for first-time users.
4. CSS pseudo-element labels on aria-labelled webcam-control buttons
so the start/stop icons inside the gradio Image component show
visible 'Start' / 'Stop' text.
Verified locally: build_demo() returns Blocks with 31 components, no
import errors with the pinned dep set (gradio 4.44.1 + starlette<0.41
+ huggingface_hub<0.30 + jinja2<3.2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- requirements.txt +1 -1
- signbridge/space.py +66 -2
|
@@ -2,7 +2,7 @@
|
|
| 2 |
gradio==4.44.1
|
| 3 |
gradio-client==1.3.0
|
| 4 |
pydantic>=2.9,<2.10
|
| 5 |
-
huggingface_hub>=0.26,<
|
| 6 |
# Pin starlette to a version where TemplateResponse(name, context) still
|
| 7 |
# works β starlette 0.41+ swapped the signature to (request, name, context)
|
| 8 |
# which breaks gradio 4.44.1's jinja2 cache key (TypeError: unhashable dict).
|
|
|
|
| 2 |
gradio==4.44.1
|
| 3 |
gradio-client==1.3.0
|
| 4 |
pydantic>=2.9,<2.10
|
| 5 |
+
huggingface_hub>=0.26,<0.30
|
| 6 |
# Pin starlette to a version where TemplateResponse(name, context) still
|
| 7 |
# works β starlette 0.41+ swapped the signature to (request, name, context)
|
| 8 |
# which breaks gradio 4.44.1's jinja2 cache key (TypeError: unhashable dict).
|
|
@@ -183,8 +183,47 @@ def _clear(state: _SessionState) -> tuple[str, str, str, None, _SessionState]:
|
|
| 183 |
return "", _format_history(state.sign_history), "", None, state
|
| 184 |
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
def build_demo() -> gr.Blocks:
|
| 187 |
-
with gr.Blocks(
|
|
|
|
|
|
|
| 188 |
gr.Markdown(
|
| 189 |
"# π€ SignBridge β real-time ASL β English speech\n"
|
| 190 |
"Two people who couldn't communicate, now can. **Snapshot** for "
|
|
@@ -204,12 +243,28 @@ def build_demo() -> gr.Blocks:
|
|
| 204 |
with gr.Tab("Snapshot β fingerspelling"):
|
| 205 |
with gr.Row():
|
| 206 |
with gr.Column(scale=3):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
webcam = gr.Image(
|
| 208 |
sources=["webcam"],
|
| 209 |
-
streaming=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
label="Sign here",
|
| 211 |
height=420,
|
| 212 |
type="numpy",
|
|
|
|
| 213 |
)
|
| 214 |
with gr.Row():
|
| 215 |
capture_btn = gr.Button(
|
|
@@ -270,10 +325,19 @@ def build_demo() -> gr.Blocks:
|
|
| 270 |
"The recognizer samples 4 frames from the clip and uses "
|
| 271 |
"motion across them to decide."
|
| 272 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
video_in = gr.Video(
|
| 274 |
sources=["webcam"],
|
| 275 |
label="Hold while signing",
|
| 276 |
height=420,
|
|
|
|
| 277 |
)
|
| 278 |
with gr.Row():
|
| 279 |
submit_video_btn = gr.Button(
|
|
|
|
| 183 |
return "", _format_history(state.sign_history), "", None, state
|
| 184 |
|
| 185 |
|
| 186 |
+
_WEBCAM_BUTTON_LABEL_CSS = """
|
| 187 |
+
/* Gradio's gr.Image webcam shows an unlabelled webcam-icon (start) and
|
| 188 |
+
red-square (stop) inside the preview. Add visible text labels via CSS
|
| 189 |
+
pseudo-elements so first-time users know what each button does. */
|
| 190 |
+
.signbridge-webcam .source-selection .icon-with-text,
|
| 191 |
+
.signbridge-webcam button[aria-label*="webcam" i]::after,
|
| 192 |
+
.signbridge-webcam button[aria-label*="record" i]::after {
|
| 193 |
+
content: " Start";
|
| 194 |
+
margin-left: 6px;
|
| 195 |
+
font-size: 14px;
|
| 196 |
+
font-weight: 600;
|
| 197 |
+
color: #4f46e5;
|
| 198 |
+
}
|
| 199 |
+
.signbridge-webcam button[aria-label*="stop" i]::after {
|
| 200 |
+
content: " Stop";
|
| 201 |
+
margin-left: 6px;
|
| 202 |
+
font-size: 14px;
|
| 203 |
+
font-weight: 600;
|
| 204 |
+
color: #dc2626;
|
| 205 |
+
}
|
| 206 |
+
/* Make any webcam-control button render its aria-label as visible text. */
|
| 207 |
+
.signbridge-webcam .controls button {
|
| 208 |
+
min-width: 80px;
|
| 209 |
+
}
|
| 210 |
+
/* Floating tooltip over the webcam pane on first load. */
|
| 211 |
+
.signbridge-webcam-help {
|
| 212 |
+
background: #eef2ff;
|
| 213 |
+
border-left: 4px solid #4f46e5;
|
| 214 |
+
padding: 8px 12px;
|
| 215 |
+
margin: 6px 0 12px 0;
|
| 216 |
+
border-radius: 6px;
|
| 217 |
+
font-size: 13px;
|
| 218 |
+
color: #1e1b4b;
|
| 219 |
+
}
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
|
| 223 |
def build_demo() -> gr.Blocks:
|
| 224 |
+
with gr.Blocks(
|
| 225 |
+
title="SignBridge", theme=gr.themes.Soft(), css=_WEBCAM_BUTTON_LABEL_CSS
|
| 226 |
+
) as demo:
|
| 227 |
gr.Markdown(
|
| 228 |
"# π€ SignBridge β real-time ASL β English speech\n"
|
| 229 |
"Two people who couldn't communicate, now can. **Snapshot** for "
|
|
|
|
| 243 |
with gr.Tab("Snapshot β fingerspelling"):
|
| 244 |
with gr.Row():
|
| 245 |
with gr.Column(scale=3):
|
| 246 |
+
gr.HTML(
|
| 247 |
+
'<div class="signbridge-webcam-help">'
|
| 248 |
+
'<b>How it works:</b> click the webcam icon to '
|
| 249 |
+
'<b>Start</b> the camera, sign a letter, then '
|
| 250 |
+
'press <b>β Capture sign</b> below. Click the '
|
| 251 |
+
'red square to <b>Stop</b> the camera.'
|
| 252 |
+
"</div>"
|
| 253 |
+
)
|
| 254 |
webcam = gr.Image(
|
| 255 |
sources=["webcam"],
|
| 256 |
+
# NOTE: streaming=True is intentionally OFF here.
|
| 257 |
+
# With it on, gradio's button-click handlers don't
|
| 258 |
+
# receive the current frame β the input value stays
|
| 259 |
+
# at the initial None until a stream event fires.
|
| 260 |
+
# Without it, the user sees a live webcam preview
|
| 261 |
+
# AND the "Capture sign" click reliably sends the
|
| 262 |
+
# current frame as the input.
|
| 263 |
+
streaming=False,
|
| 264 |
label="Sign here",
|
| 265 |
height=420,
|
| 266 |
type="numpy",
|
| 267 |
+
elem_classes=["signbridge-webcam"],
|
| 268 |
)
|
| 269 |
with gr.Row():
|
| 270 |
capture_btn = gr.Button(
|
|
|
|
| 325 |
"The recognizer samples 4 frames from the clip and uses "
|
| 326 |
"motion across them to decide."
|
| 327 |
)
|
| 328 |
+
gr.HTML(
|
| 329 |
+
'<div class="signbridge-webcam-help">'
|
| 330 |
+
'<b>Click the red record button to <span style="color:#dc2626">'
|
| 331 |
+
'Start</span></b>, hold while signing, then '
|
| 332 |
+
'<b>click again to <span style="color:#4f46e5">Stop</span></b>. '
|
| 333 |
+
'Press <b>π¬ Submit recording</b> below.'
|
| 334 |
+
"</div>"
|
| 335 |
+
)
|
| 336 |
video_in = gr.Video(
|
| 337 |
sources=["webcam"],
|
| 338 |
label="Hold while signing",
|
| 339 |
height=420,
|
| 340 |
+
elem_classes=["signbridge-webcam"],
|
| 341 |
)
|
| 342 |
with gr.Row():
|
| 343 |
submit_video_btn = gr.Button(
|