Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,7 +7,6 @@ import tempfile
|
|
| 7 |
import asyncio
|
| 8 |
import threading
|
| 9 |
import time
|
| 10 |
-
import numpy as np
|
| 11 |
import base64
|
| 12 |
from io import BytesIO
|
| 13 |
|
|
@@ -56,9 +55,20 @@ def text_to_speech(text: str) -> str:
|
|
| 56 |
return path
|
| 57 |
|
| 58 |
|
| 59 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
h = image_hash(image)
|
| 61 |
if h == last_caption["hash"] and last_caption["text"]:
|
|
|
|
| 62 |
return gr.update(), gr.update()
|
| 63 |
|
| 64 |
task_map = {
|
|
@@ -69,7 +79,6 @@ def caption_from_pil(image: Image.Image, task_choice: str):
|
|
| 69 |
|
| 70 |
t0 = time.time()
|
| 71 |
inputs = processor(text=task, images=image, return_tensors="pt").to(device)
|
| 72 |
-
|
| 73 |
with torch.inference_mode():
|
| 74 |
output_ids = model.generate(
|
| 75 |
input_ids=inputs["input_ids"],
|
|
@@ -78,15 +87,13 @@ def caption_from_pil(image: Image.Image, task_choice: str):
|
|
| 78 |
do_sample=False,
|
| 79 |
num_beams=1,
|
| 80 |
)
|
| 81 |
-
|
| 82 |
generated_text = processor.batch_decode(output_ids, skip_special_tokens=False)[0]
|
| 83 |
result = processor.post_process_generation(
|
| 84 |
-
generated_text,
|
| 85 |
-
task=task,
|
| 86 |
image_size=(image.width, image.height),
|
| 87 |
)
|
| 88 |
caption = result[task]
|
| 89 |
-
print(f"
|
| 90 |
|
| 91 |
last_caption["text"] = caption
|
| 92 |
last_caption["hash"] = h
|
|
@@ -95,37 +102,14 @@ def caption_from_pil(image: Image.Image, task_choice: str):
|
|
| 95 |
return caption, audio_path
|
| 96 |
|
| 97 |
|
| 98 |
-
def
|
| 99 |
-
"""
|
| 100 |
-
if not frame_b64 or frame_b64 == "none":
|
| 101 |
-
return gr.update(), gr.update()
|
| 102 |
-
try:
|
| 103 |
-
# Strip data URL header if present
|
| 104 |
-
if "," in frame_b64:
|
| 105 |
-
frame_b64 = frame_b64.split(",")[1]
|
| 106 |
-
img_bytes = base64.b64decode(frame_b64)
|
| 107 |
-
image = Image.open(BytesIO(img_bytes)).convert("RGB")
|
| 108 |
-
return caption_from_pil(image, task_choice)
|
| 109 |
-
except Exception as e:
|
| 110 |
-
print(f"Frame error: {e}")
|
| 111 |
-
return gr.update(), gr.update()
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def describe_upload(image, task_choice):
|
| 115 |
-
"""Streaming version for manual/upload."""
|
| 116 |
if image is None:
|
| 117 |
-
yield "Please
|
| 118 |
return
|
| 119 |
-
|
| 120 |
if not isinstance(image, Image.Image):
|
| 121 |
image = Image.fromarray(image)
|
| 122 |
|
| 123 |
-
h = image_hash(image)
|
| 124 |
-
if h == last_caption["hash"] and last_caption["text"]:
|
| 125 |
-
audio_path = text_to_speech(last_caption["text"])
|
| 126 |
-
yield last_caption["text"], audio_path
|
| 127 |
-
return
|
| 128 |
-
|
| 129 |
task_map = {
|
| 130 |
"Quick (faster)": "<CAPTION>",
|
| 131 |
"Detailed (slower)": "<MORE_DETAILED_CAPTION>",
|
|
@@ -133,7 +117,6 @@ def describe_upload(image, task_choice):
|
|
| 133 |
task = task_map.get(task_choice, "<CAPTION>")
|
| 134 |
|
| 135 |
inputs = processor(text=task, images=image, return_tensors="pt").to(device)
|
| 136 |
-
|
| 137 |
with torch.inference_mode():
|
| 138 |
output_ids = model.generate(
|
| 139 |
input_ids=inputs["input_ids"],
|
|
@@ -142,92 +125,127 @@ def describe_upload(image, task_choice):
|
|
| 142 |
do_sample=False,
|
| 143 |
num_beams=1,
|
| 144 |
)
|
| 145 |
-
|
| 146 |
generated_text = processor.batch_decode(output_ids, skip_special_tokens=False)[0]
|
| 147 |
result = processor.post_process_generation(
|
| 148 |
-
generated_text,
|
| 149 |
-
task=task,
|
| 150 |
image_size=(image.width, image.height),
|
| 151 |
)
|
| 152 |
caption = result[task]
|
| 153 |
-
|
| 154 |
last_caption["text"] = caption
|
| 155 |
-
last_caption["hash"] =
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
for word in words:
|
| 160 |
-
partial += ("" if partial == "" else " ") + word
|
| 161 |
-
yield partial, None
|
| 162 |
|
| 163 |
audio_path = text_to_speech(caption)
|
| 164 |
yield caption, audio_path
|
| 165 |
|
| 166 |
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
WEBCAM_JS = """
|
| 169 |
<script>
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
function
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
| 177 |
}
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
captureAndSend(); // immediate first capture
|
| 205 |
-
realtimeTimer = setInterval(captureAndSend, 3000);
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
function stopRealtime() {
|
| 209 |
-
if (realtimeTimer) {
|
| 210 |
-
clearInterval(realtimeTimer);
|
| 211 |
-
realtimeTimer = null;
|
| 212 |
-
console.log('Realtime stopped');
|
| 213 |
}
|
| 214 |
-
}
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
</script>
|
| 220 |
"""
|
| 221 |
|
| 222 |
|
| 223 |
with gr.Blocks(title="EchoLens RT", theme=gr.themes.Soft()) as demo:
|
|
|
|
| 224 |
gr.HTML(WEBCAM_JS)
|
| 225 |
-
gr.Markdown("# ποΈ EchoLens β Realtime Vision Assistant")
|
| 226 |
-
gr.Markdown("For blind and visually impaired users. Press **Start Realtime** to auto-describe every 3 seconds.")
|
| 227 |
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
with gr.Row():
|
|
|
|
| 231 |
with gr.Column(scale=1):
|
| 232 |
webcam_input = gr.Image(
|
| 233 |
label="Live Camera",
|
|
@@ -235,57 +253,58 @@ with gr.Blocks(title="EchoLens RT", theme=gr.themes.Soft()) as demo:
|
|
| 235 |
sources=["webcam"],
|
| 236 |
)
|
| 237 |
upload_input = gr.Image(
|
| 238 |
-
label="Or Upload Image",
|
| 239 |
type="numpy",
|
| 240 |
sources=["upload"],
|
| 241 |
)
|
| 242 |
task_choice = gr.Radio(
|
| 243 |
choices=["Quick (faster)", "Detailed (slower)"],
|
| 244 |
value="Quick (faster)",
|
| 245 |
-
label="Caption
|
| 246 |
)
|
| 247 |
with gr.Row():
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
with gr.Column(scale=1):
|
| 256 |
caption_out = gr.Textbox(
|
| 257 |
label="Caption",
|
| 258 |
-
lines=
|
| 259 |
interactive=False,
|
| 260 |
show_copy_button=True,
|
|
|
|
| 261 |
)
|
| 262 |
audio_out = gr.Audio(
|
| 263 |
label="Audio Description",
|
| 264 |
type="filepath",
|
| 265 |
autoplay=True,
|
| 266 |
)
|
| 267 |
-
gr.Markdown("*Realtime mode
|
| 268 |
|
| 269 |
-
# Hidden
|
| 270 |
with gr.Row(visible=False):
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
)
|
| 275 |
-
frame_btn = gr.Button(
|
| 276 |
-
"submit frame",
|
| 277 |
-
elem_id="frame-submit",
|
| 278 |
-
)
|
| 279 |
-
|
| 280 |
-
# Manual describe once (webcam snapshot)
|
| 281 |
-
btn.click(
|
| 282 |
-
fn=describe_upload,
|
| 283 |
-
inputs=[webcam_input, task_choice],
|
| 284 |
-
outputs=[caption_out, audio_out],
|
| 285 |
-
show_progress=False,
|
| 286 |
-
)
|
| 287 |
|
| 288 |
-
# Upload
|
| 289 |
upload_input.change(
|
| 290 |
fn=describe_upload,
|
| 291 |
inputs=[upload_input, task_choice],
|
|
@@ -293,37 +312,13 @@ with gr.Blocks(title="EchoLens RT", theme=gr.themes.Soft()) as demo:
|
|
| 293 |
show_progress=False,
|
| 294 |
)
|
| 295 |
|
| 296 |
-
# Hidden frame button β
|
| 297 |
frame_btn.click(
|
| 298 |
fn=describe_frame,
|
| 299 |
-
inputs=[
|
| 300 |
outputs=[caption_out, audio_out],
|
| 301 |
show_progress=False,
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
# Realtime toggle: update button label + call JS start/stop
|
| 305 |
-
realtime_btn.click(
|
| 306 |
-
fn=lambda s: (
|
| 307 |
-
not s,
|
| 308 |
-
gr.update(
|
| 309 |
-
value="βΉ Stop Realtime" if not s else "βΆ Start Realtime",
|
| 310 |
-
variant="stop" if not s else "secondary",
|
| 311 |
-
),
|
| 312 |
-
),
|
| 313 |
-
inputs=[is_realtime],
|
| 314 |
-
outputs=[is_realtime, realtime_btn],
|
| 315 |
-
).then(
|
| 316 |
-
fn=None,
|
| 317 |
-
js="""
|
| 318 |
-
(is_realtime) => {
|
| 319 |
-
if (is_realtime) {
|
| 320 |
-
window.startRealtime();
|
| 321 |
-
} else {
|
| 322 |
-
window.stopRealtime();
|
| 323 |
-
}
|
| 324 |
-
}
|
| 325 |
-
""",
|
| 326 |
-
inputs=[is_realtime],
|
| 327 |
)
|
| 328 |
|
| 329 |
if __name__ == "__main__":
|
|
|
|
| 7 |
import asyncio
|
| 8 |
import threading
|
| 9 |
import time
|
|
|
|
| 10 |
import base64
|
| 11 |
from io import BytesIO
|
| 12 |
|
|
|
|
| 55 |
return path
|
| 56 |
|
| 57 |
|
| 58 |
+
def describe_frame(frame_b64: str, task_choice: str):
|
| 59 |
+
"""Called every 3s by JS via hidden button. Receives raw base64 jpeg."""
|
| 60 |
+
if not frame_b64 or frame_b64 == "none" or "," not in frame_b64:
|
| 61 |
+
return gr.update(), gr.update()
|
| 62 |
+
try:
|
| 63 |
+
img_bytes = base64.b64decode(frame_b64.split(",")[1])
|
| 64 |
+
image = Image.open(BytesIO(img_bytes)).convert("RGB")
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"Decode error: {e}")
|
| 67 |
+
return gr.update(), gr.update()
|
| 68 |
+
|
| 69 |
h = image_hash(image)
|
| 70 |
if h == last_caption["hash"] and last_caption["text"]:
|
| 71 |
+
print("Same frame, skipping.")
|
| 72 |
return gr.update(), gr.update()
|
| 73 |
|
| 74 |
task_map = {
|
|
|
|
| 79 |
|
| 80 |
t0 = time.time()
|
| 81 |
inputs = processor(text=task, images=image, return_tensors="pt").to(device)
|
|
|
|
| 82 |
with torch.inference_mode():
|
| 83 |
output_ids = model.generate(
|
| 84 |
input_ids=inputs["input_ids"],
|
|
|
|
| 87 |
do_sample=False,
|
| 88 |
num_beams=1,
|
| 89 |
)
|
|
|
|
| 90 |
generated_text = processor.batch_decode(output_ids, skip_special_tokens=False)[0]
|
| 91 |
result = processor.post_process_generation(
|
| 92 |
+
generated_text, task=task,
|
|
|
|
| 93 |
image_size=(image.width, image.height),
|
| 94 |
)
|
| 95 |
caption = result[task]
|
| 96 |
+
print(f"[{time.time()-t0:.2f}s] {caption}")
|
| 97 |
|
| 98 |
last_caption["text"] = caption
|
| 99 |
last_caption["hash"] = h
|
|
|
|
| 102 |
return caption, audio_path
|
| 103 |
|
| 104 |
|
| 105 |
+
def describe_once(image, task_choice):
|
| 106 |
+
"""Manual describe with word streaming."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
if image is None:
|
| 108 |
+
yield "Please open the camera first.", None
|
| 109 |
return
|
|
|
|
| 110 |
if not isinstance(image, Image.Image):
|
| 111 |
image = Image.fromarray(image)
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
task_map = {
|
| 114 |
"Quick (faster)": "<CAPTION>",
|
| 115 |
"Detailed (slower)": "<MORE_DETAILED_CAPTION>",
|
|
|
|
| 117 |
task = task_map.get(task_choice, "<CAPTION>")
|
| 118 |
|
| 119 |
inputs = processor(text=task, images=image, return_tensors="pt").to(device)
|
|
|
|
| 120 |
with torch.inference_mode():
|
| 121 |
output_ids = model.generate(
|
| 122 |
input_ids=inputs["input_ids"],
|
|
|
|
| 125 |
do_sample=False,
|
| 126 |
num_beams=1,
|
| 127 |
)
|
|
|
|
| 128 |
generated_text = processor.batch_decode(output_ids, skip_special_tokens=False)[0]
|
| 129 |
result = processor.post_process_generation(
|
| 130 |
+
generated_text, task=task,
|
|
|
|
| 131 |
image_size=(image.width, image.height),
|
| 132 |
)
|
| 133 |
caption = result[task]
|
|
|
|
| 134 |
last_caption["text"] = caption
|
| 135 |
+
last_caption["hash"] = image_hash(image)
|
| 136 |
|
| 137 |
+
for word in caption.split():
|
| 138 |
+
yield (caption[:caption.index(word) + len(word)]), None
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
audio_path = text_to_speech(caption)
|
| 141 |
yield caption, audio_path
|
| 142 |
|
| 143 |
|
| 144 |
+
def describe_upload(image, task_choice):
|
| 145 |
+
"""Upload image describe with streaming."""
|
| 146 |
+
if image is None:
|
| 147 |
+
yield "Please upload an image.", None
|
| 148 |
+
return
|
| 149 |
+
yield from describe_once(image, task_choice)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
WEBCAM_JS = """
|
| 153 |
<script>
|
| 154 |
+
(function() {
|
| 155 |
+
let realtimeTimer = null;
|
| 156 |
+
let isRunning = false;
|
| 157 |
+
|
| 158 |
+
function getVideo() {
|
| 159 |
+
// Gradio renders webcam inside shadow DOM or iframe β try all videos
|
| 160 |
+
const videos = document.querySelectorAll('video');
|
| 161 |
+
for (const v of videos) {
|
| 162 |
+
if (v.videoWidth > 0 && v.readyState >= 2) return v;
|
| 163 |
+
}
|
| 164 |
+
return null;
|
| 165 |
+
}
|
| 166 |
|
| 167 |
+
function setNativeValue(el, value) {
|
| 168 |
+
const setter = Object.getOwnPropertyDescriptor(
|
| 169 |
+
window.HTMLTextAreaElement.prototype, 'value'
|
| 170 |
+
).set;
|
| 171 |
+
setter.call(el, value);
|
| 172 |
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
| 173 |
}
|
| 174 |
+
|
| 175 |
+
function captureAndSend() {
|
| 176 |
+
const video = getVideo();
|
| 177 |
+
if (!video) {
|
| 178 |
+
console.warn('[EchoLens] No active video found');
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
const canvas = document.createElement('canvas');
|
| 182 |
+
canvas.width = video.videoWidth;
|
| 183 |
+
canvas.height = video.videoHeight;
|
| 184 |
+
canvas.getContext('2d').drawImage(video, 0, 0);
|
| 185 |
+
const b64 = canvas.toDataURL('image/jpeg', 0.75);
|
| 186 |
+
|
| 187 |
+
const box = document.querySelector('#frame-box textarea');
|
| 188 |
+
if (!box) { console.warn('[EchoLens] No frame-box textarea'); return; }
|
| 189 |
+
setNativeValue(box, b64);
|
| 190 |
+
|
| 191 |
+
setTimeout(() => {
|
| 192 |
+
const btn = document.querySelector('#frame-btn button');
|
| 193 |
+
if (btn) {
|
| 194 |
+
btn.click();
|
| 195 |
+
console.log('[EchoLens] Frame sent');
|
| 196 |
+
} else {
|
| 197 |
+
console.warn('[EchoLens] No frame-btn button');
|
| 198 |
+
}
|
| 199 |
+
}, 150);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
}
|
|
|
|
| 201 |
|
| 202 |
+
window.echoToggleRealtime = function() {
|
| 203 |
+
const toggleBtn = document.querySelector('#rt-btn button');
|
| 204 |
+
if (!isRunning) {
|
| 205 |
+
isRunning = true;
|
| 206 |
+
if (toggleBtn) {
|
| 207 |
+
toggleBtn.textContent = 'βΉ Stop Realtime';
|
| 208 |
+
toggleBtn.style.background = '#ef4444';
|
| 209 |
+
toggleBtn.style.color = 'white';
|
| 210 |
+
}
|
| 211 |
+
captureAndSend();
|
| 212 |
+
realtimeTimer = setInterval(captureAndSend, 3000);
|
| 213 |
+
console.log('[EchoLens] Realtime ON');
|
| 214 |
+
} else {
|
| 215 |
+
isRunning = false;
|
| 216 |
+
clearInterval(realtimeTimer);
|
| 217 |
+
realtimeTimer = null;
|
| 218 |
+
if (toggleBtn) {
|
| 219 |
+
toggleBtn.textContent = 'βΆ Start Realtime';
|
| 220 |
+
toggleBtn.style.background = '';
|
| 221 |
+
toggleBtn.style.color = '';
|
| 222 |
+
}
|
| 223 |
+
console.log('[EchoLens] Realtime OFF');
|
| 224 |
+
}
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
window.echoDescribeOnce = function() {
|
| 228 |
+
captureAndSend();
|
| 229 |
+
};
|
| 230 |
+
})();
|
| 231 |
</script>
|
| 232 |
"""
|
| 233 |
|
| 234 |
|
| 235 |
with gr.Blocks(title="EchoLens RT", theme=gr.themes.Soft()) as demo:
|
| 236 |
+
|
| 237 |
gr.HTML(WEBCAM_JS)
|
|
|
|
|
|
|
| 238 |
|
| 239 |
+
gr.Markdown("""
|
| 240 |
+
# ποΈ EchoLens β Realtime Vision Assistant
|
| 241 |
+
**For blind and visually impaired users.**
|
| 242 |
+
- Open your camera below
|
| 243 |
+
- Press **Describe Once** for a single description
|
| 244 |
+
- Press **Start Realtime** to auto-describe every 3 seconds
|
| 245 |
+
""")
|
| 246 |
|
| 247 |
with gr.Row():
|
| 248 |
+
# ββ LEFT: camera ββββββββββββββββββββββββββββββββββββββ
|
| 249 |
with gr.Column(scale=1):
|
| 250 |
webcam_input = gr.Image(
|
| 251 |
label="Live Camera",
|
|
|
|
| 253 |
sources=["webcam"],
|
| 254 |
)
|
| 255 |
upload_input = gr.Image(
|
| 256 |
+
label="Or Upload an Image",
|
| 257 |
type="numpy",
|
| 258 |
sources=["upload"],
|
| 259 |
)
|
| 260 |
task_choice = gr.Radio(
|
| 261 |
choices=["Quick (faster)", "Detailed (slower)"],
|
| 262 |
value="Quick (faster)",
|
| 263 |
+
label="Caption detail",
|
| 264 |
)
|
| 265 |
with gr.Row():
|
| 266 |
+
# These buttons call JS directly via elem_id
|
| 267 |
+
gr.HTML("""
|
| 268 |
+
<div style="display:flex; gap:8px; margin-top:4px;">
|
| 269 |
+
<button
|
| 270 |
+
onclick="window.echoDescribeOnce()"
|
| 271 |
+
style="flex:1; padding:10px; background:#6366f1; color:white;
|
| 272 |
+
border:none; border-radius:8px; font-size:15px; cursor:pointer;">
|
| 273 |
+
πΈ Describe Once
|
| 274 |
+
</button>
|
| 275 |
+
<button
|
| 276 |
+
id="rt-btn-inner"
|
| 277 |
+
onclick="window.echoToggleRealtime()"
|
| 278 |
+
style="flex:1; padding:10px; background:#10b981; color:white;
|
| 279 |
+
border:none; border-radius:8px; font-size:15px; cursor:pointer;">
|
| 280 |
+
βΆ Start Realtime
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
""")
|
| 284 |
+
|
| 285 |
+
# ββ RIGHT: output ββββββββββββββββββββββββββββββββββββββ
|
| 286 |
with gr.Column(scale=1):
|
| 287 |
caption_out = gr.Textbox(
|
| 288 |
label="Caption",
|
| 289 |
+
lines=5,
|
| 290 |
interactive=False,
|
| 291 |
show_copy_button=True,
|
| 292 |
+
placeholder="Caption will appear here...",
|
| 293 |
)
|
| 294 |
audio_out = gr.Audio(
|
| 295 |
label="Audio Description",
|
| 296 |
type="filepath",
|
| 297 |
autoplay=True,
|
| 298 |
)
|
| 299 |
+
gr.Markdown("*Realtime mode auto-describes every 3 seconds.*")
|
| 300 |
|
| 301 |
+
# ββ Hidden plumbing: JS β Python ββββββββββββββββββββββββββ
|
| 302 |
with gr.Row(visible=False):
|
| 303 |
+
frame_box = gr.Textbox(elem_id="frame-box", label="frame_box")
|
| 304 |
+
with gr.Column(elem_id="frame-btn"):
|
| 305 |
+
frame_btn = gr.Button("send", elem_id="frame-submit")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
+
# Upload describe
|
| 308 |
upload_input.change(
|
| 309 |
fn=describe_upload,
|
| 310 |
inputs=[upload_input, task_choice],
|
|
|
|
| 312 |
show_progress=False,
|
| 313 |
)
|
| 314 |
|
| 315 |
+
# Hidden frame button β describe_frame (realtime + describe-once via JS)
|
| 316 |
frame_btn.click(
|
| 317 |
fn=describe_frame,
|
| 318 |
+
inputs=[frame_box, task_choice],
|
| 319 |
outputs=[caption_out, audio_out],
|
| 320 |
show_progress=False,
|
| 321 |
+
queue=True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
)
|
| 323 |
|
| 324 |
if __name__ == "__main__":
|