LovnishVerma commited on
Commit
a45c38f
Β·
verified Β·
1 Parent(s): 46cbad1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -149
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 caption_from_pil(image: Image.Image, task_choice: str):
 
 
 
 
 
 
 
 
 
 
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"Caption ({time.time()-t0:.2f}s): {caption}")
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 describe_frame(frame_b64: str, task_choice: str):
99
- """Receives base64 jpeg from JS webcam snapshot."""
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 upload or capture an image.", None
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"] = h
156
 
157
- words = caption.split()
158
- partial = ""
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
- # JS captures webcam frame β†’ puts base64 in hidden textbox β†’ triggers hidden button
 
 
 
 
 
 
 
168
  WEBCAM_JS = """
169
  <script>
170
- let realtimeTimer = null;
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- function captureAndSend() {
173
- const video = document.querySelector('video');
174
- if (!video || video.readyState < 2) {
175
- console.log('Video not ready');
176
- return;
 
177
  }
178
- const canvas = document.createElement('canvas');
179
- canvas.width = video.videoWidth || 640;
180
- canvas.height = video.videoHeight || 480;
181
- canvas.getContext('2d').drawImage(video, 0, 0);
182
- const b64 = canvas.toDataURL('image/jpeg', 0.7);
183
-
184
- // Put base64 into hidden textbox
185
- const hiddenBox = document.querySelector('#frame-input textarea');
186
- if (!hiddenBox) { console.log('No hidden box'); return; }
187
-
188
- // Set value via React/Svelte-compatible input event
189
- const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
190
- nativeInputValueSetter.call(hiddenBox, b64);
191
- hiddenBox.dispatchEvent(new Event('input', { bubbles: true }));
192
-
193
- // Click hidden submit button
194
- setTimeout(() => {
195
- const btn = document.querySelector('#frame-submit');
196
- if (btn) btn.click();
197
- else console.log('No submit btn');
198
- }, 100);
199
- }
200
-
201
- function startRealtime() {
202
- if (realtimeTimer) return;
203
- console.log('Realtime started');
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
- // Expose globally so Gradio buttons can call them
217
- window.startRealtime = startRealtime;
218
- window.stopRealtime = stopRealtime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- is_realtime = gr.State(False)
 
 
 
 
 
 
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 mode",
246
  )
247
  with gr.Row():
248
- btn = gr.Button("Describe Once β–Ά", variant="primary")
249
- realtime_btn = gr.Button(
250
- "β–Ά Start Realtime",
251
- variant="secondary",
252
- elem_id="realtime-toggle-btn",
253
- )
254
-
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  with gr.Column(scale=1):
256
  caption_out = gr.Textbox(
257
  label="Caption",
258
- lines=4,
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 captures from webcam every 3 seconds.*")
268
 
269
- # Hidden components: JS writes frame here, triggers caption
270
  with gr.Row(visible=False):
271
- frame_input = gr.Textbox(
272
- elem_id="frame-input",
273
- label="frame",
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 image
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 β†’ caption
297
  frame_btn.click(
298
  fn=describe_frame,
299
- inputs=[frame_input, task_choice],
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__":