ysharma HF Staff commited on
Commit
bff45b0
Β·
verified Β·
1 Parent(s): f8f0519

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +65 -31
app.py CHANGED
@@ -3,7 +3,6 @@ Agentic Coding : 3D Camera View Generator
3
  - Qwen Image Edit + Lightning LoRA + Multi-Angle LoRA
4
  - gr.HTML custom component (Gradio 6)
5
  - ZeroGPU (HuggingFace Spaces)
6
-
7
  """
8
 
9
  import gradio as gr
@@ -125,16 +124,15 @@ HTML_TEMPLATE = """
125
  {{#if value.img}}
126
  <img class="cv-img" src="{{value.img}}">
127
  {{else}}
128
- <div class="cv-empty">
129
  <svg class="cv-empty-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.25">
130
- <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" />
131
- <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z" />
132
  </svg>
133
- <p class="cv-empty-title">No image loaded</p>
134
- <p class="cv-empty-sub">Upload an image on the left, then hover here to see camera controls</p>
 
135
  </div>
136
  {{/if}}
137
-
138
  <div class="cv-hud">
139
  <div class="cv-readout">
140
  <span class="cv-lbl">Az</span><span class="cv-val">${value.az}&deg;</span>
@@ -162,7 +160,6 @@ HTML_TEMPLATE = """
162
 
163
  CSS_TEMPLATE = """
164
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
165
-
166
  /* ── Image well ── dark neutral so images pop, same treatment as any
167
  professional image editor / camera app preview area. Not a stylistic
168
  choice but a functional one: images render best against dark. */
@@ -174,12 +171,10 @@ CSS_TEMPLATE = """
174
  overflow: hidden;
175
  display: flex; align-items: center; justify-content: center;
176
  }
177
-
178
  .cv-img {
179
  max-width: 100%; max-height: 100%;
180
  object-fit: contain; display: block;
181
  }
182
-
183
  /* empty state */
184
  .cv-empty {
185
  text-align: center; user-select: none;
@@ -197,7 +192,26 @@ CSS_TEMPLATE = """
197
  font-size: 13px; max-width: 230px; line-height: 1.65;
198
  color: rgba(255,255,255,0.25);
199
  }
200
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  /* HUD β€” fades in on hover via CSS, no JS needed */
202
  .cv-hud {
203
  position: absolute; bottom: 16px; right: 16px;
@@ -205,7 +219,6 @@ CSS_TEMPLATE = """
205
  opacity: 0; transition: opacity 0.16s ease; pointer-events: auto;
206
  }
207
  .cv-wrap:hover .cv-hud { opacity: 1; }
208
-
209
  /* coordinate readout β€” white card floating over image */
210
  .cv-readout {
211
  display: flex; align-items: center; gap: 8px;
@@ -217,7 +230,6 @@ CSS_TEMPLATE = """
217
  .cv-lbl { color: #9ca3af; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
218
  .cv-val { color: #111827; font-weight: 600; font-variant-numeric: tabular-nums; }
219
  .cv-sep { color: #d1d5db; margin: 0 2px; }
220
-
221
  /* controls panel β€” white card, same treatment as readout */
222
  .cv-controls {
223
  display: flex; align-items: center; gap: 8px;
@@ -225,7 +237,6 @@ CSS_TEMPLATE = """
225
  border-radius: 10px; padding: 8px 10px;
226
  box-shadow: 0 2px 12px rgba(0,0,0,0.25);
227
  }
228
-
229
  /* d-pad */
230
  .cv-dpad {
231
  display: grid;
@@ -248,7 +259,6 @@ CSS_TEMPLATE = """
248
  transform: scale(1.1);
249
  }
250
  .cv-btn:active { transform: scale(0.92); background: #ffedd5; }
251
-
252
  .cv-up { grid-column:2; grid-row:1; }
253
  .cv-left { grid-column:1; grid-row:2; }
254
  .cv-dot {
@@ -258,7 +268,6 @@ CSS_TEMPLATE = """
258
  }
259
  .cv-right { grid-column:3; grid-row:2; }
260
  .cv-down { grid-column:2; grid-row:3; }
261
-
262
  /* zoom column */
263
  .cv-zoom { display: flex; flex-direction: column; gap: 3px; }
264
  .cv-zbtn {
@@ -279,7 +288,6 @@ CSS_TEMPLATE = """
279
 
280
  JS_ON_LOAD = """
281
  const DIST_STEPS = [0.6, 1.0, 1.8];
282
-
283
  function snapDist(d) {
284
  return DIST_STEPS.reduce((p, c) => Math.abs(c - d) < Math.abs(p - d) ? c : p);
285
  }
@@ -287,17 +295,53 @@ function shiftDist(d, dir) {
287
  const idx = DIST_STEPS.indexOf(snapDist(Number(d)));
288
  return DIST_STEPS[Math.max(0, Math.min(DIST_STEPS.length - 1, idx + dir))];
289
  }
290
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  // Delegated click listener β€” attached once, survives template re-renders.
292
  element.addEventListener('click', function(e) {
 
 
 
 
 
 
 
293
  const btn = e.target.closest('[data-action]');
294
  if (!btn) return;
295
-
296
  const v = Object.assign({}, props.value);
297
  let az = Number(v.az) || 0;
298
  let el = Number(v.el) || 0;
299
  let dist = Number(v.dist) || 1.0;
300
-
301
  switch (btn.dataset.action) {
302
  case 'az-minus': az = (az - 45 + 360) % 360; break;
303
  case 'az-plus': az = (az + 45) % 360; break;
@@ -306,7 +350,6 @@ element.addEventListener('click', function(e) {
306
  case 'dist-minus': dist = shiftDist(dist, -1); break;
307
  case 'dist-plus': dist = shiftDist(dist, +1); break;
308
  }
309
-
310
  props.value = { ...v, az, el, dist };
311
  trigger('submit');
312
  });
@@ -321,7 +364,6 @@ GLOBAL_CSS = """
321
  .gradio-container .row {
322
  flex-wrap: nowrap !important;
323
  }
324
-
325
  /* ── Header ── */
326
  .app-heading { padding: 28px 0 20px; }
327
  .app-heading h1 {
@@ -348,13 +390,11 @@ GLOBAL_CSS = """
348
  .app-heading .chip svg {
349
  width: 12px; height: 12px; opacity: 0.7;
350
  }
351
-
352
  /* ── Controls column β€” subtle card to separate it from viewer ── */
353
  .controls-col > .block,
354
  .controls-col > .form {
355
  background: #fafafa !important;
356
  }
357
-
358
  /* ── Camera viewer column label ── */
359
  .viewer-label {
360
  font-size: 13px; font-weight: 600;
@@ -365,7 +405,6 @@ GLOBAL_CSS = """
365
  .viewer-label .hint {
366
  font-weight: 400; color: #9ca3af; font-size: 12px;
367
  }
368
-
369
  /* ── Status display ── replaces the plain textbox look */
370
  .status-row {
371
  display: flex; align-items: center; gap: 8px;
@@ -388,14 +427,12 @@ GLOBAL_CSS = """
388
  border-color: #e5e7eb !important;
389
  resize: none !important;
390
  }
391
-
392
  /* ── Prompt box ── */
393
  .prompt-box textarea {
394
  font-family: ui-monospace, "Cascadia Code", "Source Code Pro", monospace !important;
395
  font-size: 12px !important;
396
  color: #6b7280 !important;
397
  }
398
-
399
  """
400
 
401
  GRADIO_THEME = gr.themes.Default()
@@ -404,7 +441,6 @@ GRADIO_THEME = gr.themes.Default()
404
  # ── App ────────────────────────────────────────────────────────────────────────
405
  def create_app():
406
 
407
- # FIX: theme and css are now passed to launch(), not gr.Blocks()
408
  with gr.Blocks(title="3D Camera View Generator") as demo:
409
 
410
  gr.HTML("""
@@ -456,7 +492,6 @@ def create_app():
456
  </div>
457
  """)
458
 
459
- # FIX: plain gr.HTML with dict value β€” no subclass, no inspect error
460
  cam_view = gr.HTML(
461
  value=DEFAULT_CAM_VALUE,
462
  html_template=HTML_TEMPLATE,
@@ -571,9 +606,8 @@ def create_app():
571
 
572
  if __name__ == "__main__":
573
  demo = create_app()
574
- # FIX: theme and css passed to launch() as required by Gradio 6.0
575
  demo.launch(
576
  debug=True,
577
  theme=GRADIO_THEME,
578
  css=GLOBAL_CSS,
579
- )
 
3
  - Qwen Image Edit + Lightning LoRA + Multi-Angle LoRA
4
  - gr.HTML custom component (Gradio 6)
5
  - ZeroGPU (HuggingFace Spaces)
 
6
  """
7
 
8
  import gradio as gr
 
124
  {{#if value.img}}
125
  <img class="cv-img" src="{{value.img}}">
126
  {{else}}
127
+ <div class="cv-empty cv-dropzone" data-action="upload">
128
  <svg class="cv-empty-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.25">
129
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
 
130
  </svg>
131
+ <p class="cv-empty-title">Drop an image here</p>
132
+ <p class="cv-empty-sub">or click to browse</p>
133
+ <input type="file" class="cv-file-input" accept="image/*" />
134
  </div>
135
  {{/if}}
 
136
  <div class="cv-hud">
137
  <div class="cv-readout">
138
  <span class="cv-lbl">Az</span><span class="cv-val">${value.az}&deg;</span>
 
160
 
161
  CSS_TEMPLATE = """
162
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
 
163
  /* ── Image well ── dark neutral so images pop, same treatment as any
164
  professional image editor / camera app preview area. Not a stylistic
165
  choice but a functional one: images render best against dark. */
 
171
  overflow: hidden;
172
  display: flex; align-items: center; justify-content: center;
173
  }
 
174
  .cv-img {
175
  max-width: 100%; max-height: 100%;
176
  object-fit: contain; display: block;
177
  }
 
178
  /* empty state */
179
  .cv-empty {
180
  text-align: center; user-select: none;
 
192
  font-size: 13px; max-width: 230px; line-height: 1.65;
193
  color: rgba(255,255,255,0.25);
194
  }
195
+ /* dropzone */
196
+ .cv-dropzone {
197
+ cursor: pointer;
198
+ border: 2px dashed rgba(255,255,255,0.15);
199
+ border-radius: 12px;
200
+ padding: 40px 24px;
201
+ transition: border-color 0.2s, background 0.2s;
202
+ }
203
+ .cv-dropzone:hover, .cv-dropzone.cv-drag-over {
204
+ border-color: #f97316;
205
+ background: rgba(249,115,22,0.06);
206
+ }
207
+ .cv-dropzone.cv-drag-over .cv-empty-icon {
208
+ color: #f97316;
209
+ }
210
+ .cv-file-input {
211
+ position: absolute;
212
+ width: 0; height: 0;
213
+ opacity: 0; pointer-events: none;
214
+ }
215
  /* HUD β€” fades in on hover via CSS, no JS needed */
216
  .cv-hud {
217
  position: absolute; bottom: 16px; right: 16px;
 
219
  opacity: 0; transition: opacity 0.16s ease; pointer-events: auto;
220
  }
221
  .cv-wrap:hover .cv-hud { opacity: 1; }
 
222
  /* coordinate readout β€” white card floating over image */
223
  .cv-readout {
224
  display: flex; align-items: center; gap: 8px;
 
230
  .cv-lbl { color: #9ca3af; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
231
  .cv-val { color: #111827; font-weight: 600; font-variant-numeric: tabular-nums; }
232
  .cv-sep { color: #d1d5db; margin: 0 2px; }
 
233
  /* controls panel β€” white card, same treatment as readout */
234
  .cv-controls {
235
  display: flex; align-items: center; gap: 8px;
 
237
  border-radius: 10px; padding: 8px 10px;
238
  box-shadow: 0 2px 12px rgba(0,0,0,0.25);
239
  }
 
240
  /* d-pad */
241
  .cv-dpad {
242
  display: grid;
 
259
  transform: scale(1.1);
260
  }
261
  .cv-btn:active { transform: scale(0.92); background: #ffedd5; }
 
262
  .cv-up { grid-column:2; grid-row:1; }
263
  .cv-left { grid-column:1; grid-row:2; }
264
  .cv-dot {
 
268
  }
269
  .cv-right { grid-column:3; grid-row:2; }
270
  .cv-down { grid-column:2; grid-row:3; }
 
271
  /* zoom column */
272
  .cv-zoom { display: flex; flex-direction: column; gap: 3px; }
273
  .cv-zbtn {
 
288
 
289
  JS_ON_LOAD = """
290
  const DIST_STEPS = [0.6, 1.0, 1.8];
 
291
  function snapDist(d) {
292
  return DIST_STEPS.reduce((p, c) => Math.abs(c - d) < Math.abs(p - d) ? c : p);
293
  }
 
295
  const idx = DIST_STEPS.indexOf(snapDist(Number(d)));
296
  return DIST_STEPS[Math.max(0, Math.min(DIST_STEPS.length - 1, idx + dir))];
297
  }
298
+ // --- Image upload (drag-drop + click-to-browse) ---
299
+ function loadImageFile(file) {
300
+ if (!file || !file.type.startsWith('image/')) return;
301
+ const reader = new FileReader();
302
+ reader.onload = function(e) {
303
+ const v = Object.assign({}, props.value);
304
+ props.value = { ...v, img: e.target.result };
305
+ };
306
+ reader.readAsDataURL(file);
307
+ }
308
+ element.addEventListener('dragover', function(e) {
309
+ e.preventDefault();
310
+ const dz = element.querySelector('.cv-dropzone');
311
+ if (dz) dz.classList.add('cv-drag-over');
312
+ });
313
+ element.addEventListener('dragleave', function(e) {
314
+ const dz = element.querySelector('.cv-dropzone');
315
+ if (dz) dz.classList.remove('cv-drag-over');
316
+ });
317
+ element.addEventListener('drop', function(e) {
318
+ e.preventDefault();
319
+ const dz = element.querySelector('.cv-dropzone');
320
+ if (dz) dz.classList.remove('cv-drag-over');
321
+ if (e.dataTransfer && e.dataTransfer.files.length) {
322
+ loadImageFile(e.dataTransfer.files[0]);
323
+ }
324
+ });
325
+ element.addEventListener('change', function(e) {
326
+ if (e.target.classList.contains('cv-file-input')) {
327
+ loadImageFile(e.target.files[0]);
328
+ }
329
+ });
330
  // Delegated click listener β€” attached once, survives template re-renders.
331
  element.addEventListener('click', function(e) {
332
+ // Handle dropzone click -> open file picker
333
+ const dz = e.target.closest('[data-action="upload"]');
334
+ if (dz) {
335
+ const fi = element.querySelector('.cv-file-input');
336
+ if (fi) fi.click();
337
+ return;
338
+ }
339
  const btn = e.target.closest('[data-action]');
340
  if (!btn) return;
 
341
  const v = Object.assign({}, props.value);
342
  let az = Number(v.az) || 0;
343
  let el = Number(v.el) || 0;
344
  let dist = Number(v.dist) || 1.0;
 
345
  switch (btn.dataset.action) {
346
  case 'az-minus': az = (az - 45 + 360) % 360; break;
347
  case 'az-plus': az = (az + 45) % 360; break;
 
350
  case 'dist-minus': dist = shiftDist(dist, -1); break;
351
  case 'dist-plus': dist = shiftDist(dist, +1); break;
352
  }
 
353
  props.value = { ...v, az, el, dist };
354
  trigger('submit');
355
  });
 
364
  .gradio-container .row {
365
  flex-wrap: nowrap !important;
366
  }
 
367
  /* ── Header ── */
368
  .app-heading { padding: 28px 0 20px; }
369
  .app-heading h1 {
 
390
  .app-heading .chip svg {
391
  width: 12px; height: 12px; opacity: 0.7;
392
  }
 
393
  /* ── Controls column β€” subtle card to separate it from viewer ── */
394
  .controls-col > .block,
395
  .controls-col > .form {
396
  background: #fafafa !important;
397
  }
 
398
  /* ── Camera viewer column label ── */
399
  .viewer-label {
400
  font-size: 13px; font-weight: 600;
 
405
  .viewer-label .hint {
406
  font-weight: 400; color: #9ca3af; font-size: 12px;
407
  }
 
408
  /* ── Status display ── replaces the plain textbox look */
409
  .status-row {
410
  display: flex; align-items: center; gap: 8px;
 
427
  border-color: #e5e7eb !important;
428
  resize: none !important;
429
  }
 
430
  /* ── Prompt box ── */
431
  .prompt-box textarea {
432
  font-family: ui-monospace, "Cascadia Code", "Source Code Pro", monospace !important;
433
  font-size: 12px !important;
434
  color: #6b7280 !important;
435
  }
 
436
  """
437
 
438
  GRADIO_THEME = gr.themes.Default()
 
441
  # ── App ────────────────────────────────────────────────────────────────────────
442
  def create_app():
443
 
 
444
  with gr.Blocks(title="3D Camera View Generator") as demo:
445
 
446
  gr.HTML("""
 
492
  </div>
493
  """)
494
 
 
495
  cam_view = gr.HTML(
496
  value=DEFAULT_CAM_VALUE,
497
  html_template=HTML_TEMPLATE,
 
606
 
607
  if __name__ == "__main__":
608
  demo = create_app()
 
609
  demo.launch(
610
  debug=True,
611
  theme=GRADIO_THEME,
612
  css=GLOBAL_CSS,
613
+ )