ricklon commited on
Commit
301e638
·
1 Parent(s): 7db9014

Start kiosk capture and rotate scene automatically

Browse files
src/demo/infer_single_image.py CHANGED
@@ -838,7 +838,7 @@ def _resolve_checkpoint_path(checkpoint: str | Path) -> Path:
838
 
839
 
840
  def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
841
- """Patch SuperSplat HTML so generated camera animation starts paused.
842
 
843
  Args:
844
  html_path: Generated SuperSplat HTML path.
@@ -848,14 +848,18 @@ def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
848
  """
849
  source = html_path.read_text(encoding="utf-8")
850
  old = "noanim: url.searchParams.has('noanim'),"
851
- new = "noanim: true,"
852
  if new in source:
853
  return
854
- if old not in source:
 
 
 
 
855
  raise RuntimeError(
856
- "Could not pause SuperSplat HTML animation; the viewer bundle changed."
857
  )
858
- html_path.write_text(source.replace(old, new, 1), encoding="utf-8")
859
 
860
 
861
  def patch_supersplat_html_viewer_bridge(
 
838
 
839
 
840
  def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
841
+ """Pause generated camera animation unless the viewer opts into rotation.
842
 
843
  Args:
844
  html_path: Generated SuperSplat HTML path.
 
848
  """
849
  source = html_path.read_text(encoding="utf-8")
850
  old = "noanim: url.searchParams.has('noanim'),"
851
+ new = "noanim: !url.searchParams.has('autorotate'),"
852
  if new in source:
853
  return
854
+ if old in source:
855
+ source = source.replace(old, new, 1)
856
+ elif "noanim: true," in source:
857
+ source = source.replace("noanim: true,", new, 1)
858
+ else:
859
  raise RuntimeError(
860
+ "Could not configure SuperSplat camera rotation; the viewer bundle changed."
861
  )
862
+ html_path.write_text(source, encoding="utf-8")
863
 
864
 
865
  def patch_supersplat_html_viewer_bridge(
src/demo/server_frontend/app.js CHANGED
@@ -32,6 +32,7 @@ const htmlDownload = document.getElementById("htmlDownload");
32
  const kioskCooldown = document.getElementById("kioskCooldown");
33
  const kioskCooldownTime = document.getElementById("kioskCooldownTime");
34
  const kioskStartButton = document.getElementById("kioskStartButton");
 
35
  const urlParams = new URLSearchParams(window.location.search);
36
  const kioskIntervalMinutes = Math.min(60, Math.max(1, Number.parseInt(urlParams.get("interval") || "3", 10) || 3));
37
  document.getElementById("kioskIntervalLabel").textContent = `${kioskIntervalMinutes} ${kioskIntervalMinutes === 1 ? "minute" : "minutes"}`;
@@ -60,8 +61,16 @@ const cameraFormats = {
60
  const KIOSK_COOLDOWN_DEFAULT_SECONDS = 180;
61
  const KIOSK_COOLDOWN_MAX_SECONDS = 3600;
62
  const KIOSK_COOLDOWN_STORAGE_KEY = "infinisplat:kiosk-cooldown-until";
 
63
  const KIOSK_ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
64
  const kioskMode = KIOSK_ENABLED_VALUES.has((urlParams.get("kiosk") || "").toLowerCase());
 
 
 
 
 
 
 
65
  let cooldownUntil = 0;
66
  let cooldownTotalMs = 0;
67
  let cooldownTicker = null;
@@ -360,7 +369,7 @@ async function refreshCameraDevices(activeDeviceId = "") {
360
  const devices = (await navigator.mediaDevices.enumerateDevices())
361
  .filter((device) => device.kind === "videoinput");
362
  const availableIds = new Set(devices.map((device) => device.deviceId));
363
- if (selectedCameraDeviceId && !availableIds.has(selectedCameraDeviceId)) {
364
  selectedCameraDeviceId = "";
365
  }
366
 
@@ -379,6 +388,21 @@ async function refreshCameraDevices(activeDeviceId = "") {
379
  const preferredId = selectedCameraDeviceId || activeDeviceId;
380
  cameraSelect.value = availableIds.has(preferredId) ? preferredId : "";
381
  cameraSelect.disabled = running || devices.length < 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
 
384
  async function switchCamera() {
@@ -447,6 +471,11 @@ async function startCamera() {
447
  console.error(error);
448
  if (error?.name === "NotFoundError" || error?.name === "OverconstrainedError") {
449
  selectedCameraDeviceId = "";
 
 
 
 
 
450
  }
451
  stopCamera();
452
  if (kioskMode && cameraButton.dataset.kioskStarted === "true") {
@@ -569,6 +598,19 @@ kioskStartButton.addEventListener("click", () => {
569
  startCamera();
570
  });
571
  cameraSelect.addEventListener("change", switchCamera);
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  cancelCameraButton.addEventListener("click", stopCamera);
573
  captureButton.addEventListener("click", capturePhoto);
574
  for (const input of cameraFormatInputs) input.addEventListener("change", updateCameraFrame);
@@ -602,6 +644,8 @@ navigator.mediaDevices?.addEventListener?.("devicechange", () => {
602
  if (cameraStream) {
603
  refreshCameraDevices(cameraStream.getVideoTracks()[0]?.getSettings?.().deviceId || "")
604
  .catch((error) => console.warn("Could not refresh the camera list.", error));
 
 
605
  }
606
  });
607
 
@@ -707,7 +751,7 @@ async function runPipeline() {
707
  );
708
  if (!viewer?.url) throw new Error("The viewer export returned no file.");
709
  setViewerState("loading", "Initializing 3D viewer", "Loading scene data and waiting for the first rendered frame");
710
- viewerFrame.src = `${viewer.url}?v=${Date.now()}`;
711
  reconstructed = true;
712
  if (kioskMode) document.documentElement.dataset.kioskState = "showcase";
713
 
@@ -727,6 +771,11 @@ async function runPipeline() {
727
  showToast(errorMessage(error), true);
728
  } finally {
729
  if (reconstructed) beginCooldown();
 
 
 
 
 
730
  setRunning(false);
731
  }
732
  }
@@ -737,6 +786,7 @@ async function connect() {
737
  try {
738
  client = await Client.connect(window.location.origin);
739
  reconstructButton.disabled = !selectedFile || inputsLocked();
 
740
  const template = resultValue(await client.predict("/viewer_template", {}));
741
  if (template?.url) viewerPreloader.src = template.url;
742
  } catch (error) {
@@ -750,6 +800,9 @@ if (kioskMode) {
750
  document.documentElement.dataset.kioskState = "welcome";
751
  cameraButton.dataset.kioskStarted = "true";
752
  cameraButton.textContent = "Start 3D playground";
 
 
 
753
  const deadline = storedCooldownDeadline();
754
  if (Number.isFinite(deadline) && deadline > Date.now()) {
755
  beginCooldown(kioskCooldownMs || deadline - Date.now(), deadline);
 
32
  const kioskCooldown = document.getElementById("kioskCooldown");
33
  const kioskCooldownTime = document.getElementById("kioskCooldownTime");
34
  const kioskStartButton = document.getElementById("kioskStartButton");
35
+ const kioskCameraSelect = document.getElementById("kioskCameraSelect");
36
  const urlParams = new URLSearchParams(window.location.search);
37
  const kioskIntervalMinutes = Math.min(60, Math.max(1, Number.parseInt(urlParams.get("interval") || "3", 10) || 3));
38
  document.getElementById("kioskIntervalLabel").textContent = `${kioskIntervalMinutes} ${kioskIntervalMinutes === 1 ? "minute" : "minutes"}`;
 
61
  const KIOSK_COOLDOWN_DEFAULT_SECONDS = 180;
62
  const KIOSK_COOLDOWN_MAX_SECONDS = 3600;
63
  const KIOSK_COOLDOWN_STORAGE_KEY = "infinisplat:kiosk-cooldown-until";
64
+ const KIOSK_CAMERA_STORAGE_KEY = "infinisplat:kiosk-camera-device";
65
  const KIOSK_ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
66
  const kioskMode = KIOSK_ENABLED_VALUES.has((urlParams.get("kiosk") || "").toLowerCase());
67
+ if (kioskMode) {
68
+ try {
69
+ selectedCameraDeviceId = window.localStorage.getItem(KIOSK_CAMERA_STORAGE_KEY) || "";
70
+ } catch (error) {
71
+ console.warn("The kiosk camera selection could not be read from storage.", error);
72
+ }
73
+ }
74
  let cooldownUntil = 0;
75
  let cooldownTotalMs = 0;
76
  let cooldownTicker = null;
 
369
  const devices = (await navigator.mediaDevices.enumerateDevices())
370
  .filter((device) => device.kind === "videoinput");
371
  const availableIds = new Set(devices.map((device) => device.deviceId));
372
+ if (selectedCameraDeviceId && devices.length && !availableIds.has(selectedCameraDeviceId)) {
373
  selectedCameraDeviceId = "";
374
  }
375
 
 
388
  const preferredId = selectedCameraDeviceId || activeDeviceId;
389
  cameraSelect.value = availableIds.has(preferredId) ? preferredId : "";
390
  cameraSelect.disabled = running || devices.length < 2;
391
+
392
+ kioskCameraSelect.replaceChildren();
393
+ const kioskAutomaticOption = document.createElement("option");
394
+ kioskAutomaticOption.value = "";
395
+ kioskAutomaticOption.textContent = "Automatic camera";
396
+ kioskCameraSelect.append(kioskAutomaticOption);
397
+ devices.forEach((device, index) => {
398
+ const option = document.createElement("option");
399
+ option.value = device.deviceId;
400
+ option.textContent = device.label || `Camera ${index + 1}`;
401
+ kioskCameraSelect.append(option);
402
+ });
403
+ const kioskPreferredId = selectedCameraDeviceId || activeDeviceId;
404
+ kioskCameraSelect.value = availableIds.has(kioskPreferredId) ? kioskPreferredId : "";
405
+ kioskCameraSelect.disabled = devices.length < 2 || cameraStream !== null;
406
  }
407
 
408
  async function switchCamera() {
 
471
  console.error(error);
472
  if (error?.name === "NotFoundError" || error?.name === "OverconstrainedError") {
473
  selectedCameraDeviceId = "";
474
+ try {
475
+ window.localStorage.removeItem(KIOSK_CAMERA_STORAGE_KEY);
476
+ } catch (storageError) {
477
+ console.warn("The unavailable kiosk camera selection could not be cleared.", storageError);
478
+ }
479
  }
480
  stopCamera();
481
  if (kioskMode && cameraButton.dataset.kioskStarted === "true") {
 
598
  startCamera();
599
  });
600
  cameraSelect.addEventListener("change", switchCamera);
601
+ kioskCameraSelect.addEventListener("change", () => {
602
+ selectedCameraDeviceId = kioskCameraSelect.value;
603
+ cameraSelect.value = selectedCameraDeviceId;
604
+ try {
605
+ if (selectedCameraDeviceId) {
606
+ window.localStorage.setItem(KIOSK_CAMERA_STORAGE_KEY, selectedCameraDeviceId);
607
+ } else {
608
+ window.localStorage.removeItem(KIOSK_CAMERA_STORAGE_KEY);
609
+ }
610
+ } catch (error) {
611
+ console.warn("The kiosk camera selection could not be saved.", error);
612
+ }
613
+ });
614
  cancelCameraButton.addEventListener("click", stopCamera);
615
  captureButton.addEventListener("click", capturePhoto);
616
  for (const input of cameraFormatInputs) input.addEventListener("change", updateCameraFrame);
 
644
  if (cameraStream) {
645
  refreshCameraDevices(cameraStream.getVideoTracks()[0]?.getSettings?.().deviceId || "")
646
  .catch((error) => console.warn("Could not refresh the camera list.", error));
647
+ } else if (kioskMode) {
648
+ refreshCameraDevices().catch((error) => console.warn("Could not refresh kiosk cameras.", error));
649
  }
650
  });
651
 
 
751
  );
752
  if (!viewer?.url) throw new Error("The viewer export returned no file.");
753
  setViewerState("loading", "Initializing 3D viewer", "Loading scene data and waiting for the first rendered frame");
754
+ viewerFrame.src = `${viewer.url}?v=${Date.now()}${kioskMode ? "&autorotate" : ""}`;
755
  reconstructed = true;
756
  if (kioskMode) document.documentElement.dataset.kioskState = "showcase";
757
 
 
771
  showToast(errorMessage(error), true);
772
  } finally {
773
  if (reconstructed) beginCooldown();
774
+ else if (kioskMode && cameraButton.dataset.kioskStarted === "true") {
775
+ document.documentElement.dataset.kioskState = "showcase";
776
+ window.clearTimeout(kioskCaptureTimer);
777
+ kioskCaptureTimer = window.setTimeout(startCamera, 15000);
778
+ }
779
  setRunning(false);
780
  }
781
  }
 
786
  try {
787
  client = await Client.connect(window.location.origin);
788
  reconstructButton.disabled = !selectedFile || inputsLocked();
789
+ if (kioskMode && selectedFile && !running) window.setTimeout(runPipeline, 0);
790
  const template = resultValue(await client.predict("/viewer_template", {}));
791
  if (template?.url) viewerPreloader.src = template.url;
792
  } catch (error) {
 
800
  document.documentElement.dataset.kioskState = "welcome";
801
  cameraButton.dataset.kioskStarted = "true";
802
  cameraButton.textContent = "Start 3D playground";
803
+ refreshCameraDevices().catch((error) => {
804
+ console.warn("Could not list kiosk cameras.", error);
805
+ });
806
  const deadline = storedCooldownDeadline();
807
  if (Number.isFinite(deadline) && deadline > Date.now()) {
808
  beginCooldown(kioskCooldownMs || deadline - Date.now(), deadline);
src/demo/server_frontend/index.html CHANGED
@@ -17,13 +17,14 @@
17
  document.documentElement.dataset.kiosk = "on";
18
  }
19
  </script>
20
- <link rel="stylesheet" href="/infinisplat/assets/styles.css?v=10">
21
  </head>
22
  <body>
23
  <main class="app-shell">
24
  <section class="kiosk-welcome" aria-label="Fubar Labs 3D Printing Playground">
25
  <div><span class="kiosk-eyebrow">FUBAR LABS · NEW JERSEY MAKER SPACE</span><h2>Make something <em>impossible.</em></h2>
26
- <p>Step into the 3D Printing Playground. Start the camera and it will capture a fresh photo every few minutes, turning each one into a scene you can explore in 3D.</p>
 
27
  <button class="kiosk-start" id="kioskStartButton" type="button">Start the 3D playground <span aria-hidden="true">↗</span></button></div>
28
  <div class="kiosk-instructions"><span class="kiosk-live-dot"></span><span>Live 3D showcase</span><small>New scene every <b id="kioskIntervalLabel">3 minutes</b></small></div>
29
  </section>
@@ -153,6 +154,6 @@
153
  </main>
154
 
155
  <div class="toast" id="toast" role="status" aria-live="polite"></div>
156
- <script type="module" src="/infinisplat/assets/app.js?v=12"></script>
157
  </body>
158
  </html>
 
17
  document.documentElement.dataset.kiosk = "on";
18
  }
19
  </script>
20
+ <link rel="stylesheet" href="/infinisplat/assets/styles.css?v=11">
21
  </head>
22
  <body>
23
  <main class="app-shell">
24
  <section class="kiosk-welcome" aria-label="Fubar Labs 3D Printing Playground">
25
  <div><span class="kiosk-eyebrow">FUBAR LABS · NEW JERSEY MAKER SPACE</span><h2>Make something <em>impossible.</em></h2>
26
+ <p>Choose a camera and start the 3D Printing Playground. It captures automatically, then brings every new scene to life with a rotating 3D view.</p>
27
+ <label class="kiosk-camera-picker" for="kioskCameraSelect"><span>CAMERA</span><select id="kioskCameraSelect" aria-label="Select kiosk camera"><option value="">Choose a camera…</option></select></label>
28
  <button class="kiosk-start" id="kioskStartButton" type="button">Start the 3D playground <span aria-hidden="true">↗</span></button></div>
29
  <div class="kiosk-instructions"><span class="kiosk-live-dot"></span><span>Live 3D showcase</span><small>New scene every <b id="kioskIntervalLabel">3 minutes</b></small></div>
30
  </section>
 
154
  </main>
155
 
156
  <div class="toast" id="toast" role="status" aria-live="polite"></div>
157
+ <script type="module" src="/infinisplat/assets/app.js?v=13"></script>
158
  </body>
159
  </html>
src/demo/server_frontend/styles.css CHANGED
@@ -567,6 +567,7 @@ button, input { font: inherit; }
567
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .source-panel { display: block; position: fixed; right: 24px; bottom: 24px; z-index: 10; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
568
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome { min-height: 54vh; align-items: center; justify-content: center; text-align: center; }
569
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome > div:first-child { max-width: 800px; }
 
570
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-instructions { display: none; }
571
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .workspace { display: none; }
572
  :root[data-kiosk="on"][data-kiosk-state="processing"] .kiosk-welcome { display: none; }
@@ -574,6 +575,9 @@ button, input { font: inherit; }
574
  .kiosk-welcome h2 { margin: 6px 0; font-size: clamp(1.8rem, 4vw, 3.8rem); letter-spacing: -.045em; line-height: 1; }
575
  .kiosk-welcome h2 em { color: #b8f34a; font-style: normal; }
576
  .kiosk-welcome p { max-width: 650px; margin: 9px 0 0; color: #b0beb6; font-size: clamp(.8rem, 1.15vw, 1rem); }
 
 
 
577
  .kiosk-instructions { display: grid; grid-template-columns: auto auto; align-items: center; gap: 4px 9px; flex-shrink: 0; padding: 12px 15px; border: 1px solid #35453b; border-radius: 9px; background: #121a16; color: #e3eee6; font-size: .82rem; font-weight: 700; }
578
  .kiosk-instructions small { grid-column: 2; color: #9eaea4; font-size: .7rem; font-weight: 500; }
579
  .kiosk-instructions b { color: #b8f34a; font-weight: 700; }
 
567
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .source-panel { display: block; position: fixed; right: 24px; bottom: 24px; z-index: 10; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
568
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome { min-height: 54vh; align-items: center; justify-content: center; text-align: center; }
569
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome > div:first-child { max-width: 800px; }
570
+ :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome p { margin-inline: auto; }
571
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-instructions { display: none; }
572
  :root[data-kiosk="on"][data-kiosk-state="welcome"] .workspace { display: none; }
573
  :root[data-kiosk="on"][data-kiosk-state="processing"] .kiosk-welcome { display: none; }
 
575
  .kiosk-welcome h2 { margin: 6px 0; font-size: clamp(1.8rem, 4vw, 3.8rem); letter-spacing: -.045em; line-height: 1; }
576
  .kiosk-welcome h2 em { color: #b8f34a; font-style: normal; }
577
  .kiosk-welcome p { max-width: 650px; margin: 9px 0 0; color: #b0beb6; font-size: clamp(.8rem, 1.15vw, 1rem); }
578
+ .kiosk-camera-picker { display: grid; grid-template-columns: auto minmax(180px, 280px); align-items: center; justify-content: center; gap: 12px; margin: 20px auto 0; color: #b8f34a; font-size: .68rem; font-weight: 850; letter-spacing: .15em; text-align: left; }
579
+ .kiosk-camera-picker select { min-height: 44px; padding: 0 38px 0 13px; border: 1px solid #536756; border-radius: 6px; background: #111a14; color: #f1f7f2; font: inherit; font-size: .82rem; font-weight: 600; letter-spacing: 0; }
580
+ .kiosk-camera-picker select:disabled { opacity: .6; }
581
  .kiosk-instructions { display: grid; grid-template-columns: auto auto; align-items: center; gap: 4px 9px; flex-shrink: 0; padding: 12px 15px; border: 1px solid #35453b; border-radius: 9px; background: #121a16; color: #e3eee6; font-size: .82rem; font-weight: 700; }
582
  .kiosk-instructions small { grid-column: 2; color: #9eaea4; font-size: .7rem; font-weight: 500; }
583
  .kiosk-instructions b { color: #b8f34a; font-weight: 700; }