mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 18:58:49 -05:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42bf67d207 | ||
|
|
d65fd99de4 | ||
|
|
e3d4d9af9b | ||
|
|
ab730ee8a3 | ||
|
|
8fe4f2f98e | ||
|
|
89f3a1cb65 | ||
|
|
79d36cae93 | ||
|
|
1dcd7e5d7d | ||
|
|
b4bc99ab27 | ||
|
|
c94e1a0508 | ||
|
|
abcb0871b2 | ||
|
|
1ffe680895 | ||
|
|
ed26b90384 | ||
|
|
2b48fee00f | ||
|
|
1284125fff | ||
|
|
28d0b1437d | ||
|
|
9d3b8df4bf | ||
|
|
9d42cb5581 | ||
|
|
e670226f8c | ||
|
|
0669f29a4a | ||
|
|
38d3465bf8 | ||
|
|
6bfbb8ad80 | ||
|
|
ba2f66ebc1 | ||
|
|
fde2fca0a0 | ||
|
|
d36ea4f672 | ||
|
|
4be6e6c9a2 | ||
|
|
7bac2fc447 | ||
|
|
e8a407cd49 | ||
|
|
bee1866a70 | ||
|
|
ddfc889a8c | ||
|
|
c71267873a | ||
|
|
1ba4ebe5ff | ||
|
|
607d327e9c | ||
|
|
1bdca60648 | ||
|
|
ea4af2ebb3 | ||
|
|
b2bbaf2085 | ||
|
|
18e7fb8d1b | ||
|
|
ba71106544 | ||
|
|
a395656056 | ||
|
|
50c2c1f5db | ||
|
|
b59389870e | ||
|
|
cd4aee4e74 | ||
|
|
9de0bfb1e7 | ||
|
|
008c90edf1 | ||
|
|
eed88983ca | ||
|
|
923c04736b | ||
|
|
bdc3eb640d | ||
|
|
01354c56e6 | ||
|
|
0ff03a74a5 | ||
|
|
73a78eef20 | ||
|
|
a157ece816 | ||
|
|
8cb7cc5bdc | ||
|
|
dbcb7f6a7a | ||
|
|
34936ab05c | ||
|
|
7231809140 | ||
|
|
8ca714cf1a | ||
|
|
ffd34de56c | ||
|
|
aeeff6fe1b | ||
|
|
846c9dae55 | ||
|
|
e750712a39 | ||
|
|
ff8b0c530e | ||
|
|
69c5bfe544 | ||
|
|
3ad95b376a | ||
|
|
bda740b08d | ||
|
|
3c8c5f5883 | ||
|
|
c35544e515 | ||
|
|
0df4a5ec89 | ||
|
|
8e301e7db7 | ||
|
|
b712fb89c6 | ||
|
|
fe2c29371f | ||
|
|
61ee692815 | ||
|
|
487666eed8 | ||
|
|
51b42ea778 | ||
|
|
318cad1846 | ||
|
|
f3e3b6a803 | ||
|
|
b48348d89e |
@@ -5,6 +5,7 @@
|
||||
.git
|
||||
.gitignore
|
||||
**/node_modules
|
||||
webui/.cache/mediamtx/
|
||||
# Neither the legacy state directory nor the Compose-mounted replacement may
|
||||
# enter an image build. This prevents credentials and backups from becoming
|
||||
# image layers after an operator has started using either deployment layout.
|
||||
|
||||
@@ -58,6 +58,8 @@ jobs:
|
||||
# replaced on each successful push, just as main replaces `latest`.
|
||||
- name: Build and optionally publish
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
DOCKER_BUILD_RECORD_RETENTION_DAYS: 3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
|
||||
@@ -5,6 +5,7 @@ create_2_Open_Interface_Spec.txt
|
||||
|
||||
logs
|
||||
node_modules/
|
||||
webui/.cache/mediamtx/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pio
|
||||
|
||||
+43
-19
@@ -9,15 +9,21 @@
|
||||
ARG FEDORA_VERSION=43
|
||||
|
||||
FROM fedora:${FEDORA_VERSION} AS architecture-check
|
||||
ARG FEDORA_VERSION
|
||||
ARG TARGETARCH
|
||||
# The central server is currently deployed and verified only on amd64. Failing
|
||||
# here avoids publishing an ARM image whose native workers and hardware paths
|
||||
# have never been exercised on a real ARM server.
|
||||
RUN test "${TARGETARCH}" = "amd64" || (echo "MultiRover server images support only linux/amd64." >&2; exit 1)
|
||||
|
||||
FROM architecture-check AS webui-build
|
||||
RUN dnf install -y --setopt=install_weak_deps=False nodejs npm \
|
||||
&& dnf clean all
|
||||
# Both Node builders reuse the same installed toolchain layer.
|
||||
# Fedora 43 uses DNF5. Cache metadata and RPMs outside image layers, and keep
|
||||
# downloads after installation so invalidated layers can reuse them.
|
||||
FROM architecture-check AS node-build-base
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-node-build-base,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True nodejs npm
|
||||
|
||||
FROM node-build-base AS webui-build
|
||||
WORKDIR /build
|
||||
# Copy dependency manifests first so ordinary source edits retain the expensive
|
||||
# npm cache layer. npm ci makes the checked-in lockfile the exact dependency
|
||||
@@ -25,14 +31,17 @@ WORKDIR /build
|
||||
COPY webui/package.json webui/package-lock.json ./webui/
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
cd webui && npm ci
|
||||
# Keep the pinned browser publisher in a reusable layer across UI source edits.
|
||||
COPY webui/scripts/prepareMediaMtx.js ./webui/scripts/prepareMediaMtx.js
|
||||
RUN cd webui && node scripts/prepareMediaMtx.js
|
||||
COPY webui ./webui
|
||||
# Vite deliberately emits into ../server/public. Create that destination in the
|
||||
# isolated builder and copy only its finished files into the runtime stage.
|
||||
RUN mkdir -p server/public && cd webui && npm run build
|
||||
|
||||
FROM architecture-check AS server-dependencies
|
||||
RUN dnf install -y --setopt=install_weak_deps=False nodejs npm gcc-c++ make python3 \
|
||||
&& dnf clean all
|
||||
FROM node-build-base AS server-dependencies
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-server-dependencies,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True gcc-c++ make python3
|
||||
WORKDIR /build/server
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
# Native Node modules compile here when a matching prebuild is unavailable;
|
||||
@@ -41,7 +50,8 @@ RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci --omit=dev
|
||||
|
||||
FROM architecture-check AS native-workers
|
||||
RUN dnf install -y --setopt=install_weak_deps=False \
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-native-workers,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True \
|
||||
bluez-libs-devel \
|
||||
gcc-c++ \
|
||||
libcap \
|
||||
@@ -49,8 +59,7 @@ RUN dnf install -y --setopt=install_weak_deps=False \
|
||||
libusb1-devel \
|
||||
make \
|
||||
pkgconf-pkg-config \
|
||||
wiiuse-devel \
|
||||
&& dnf clean all
|
||||
wiiuse-devel
|
||||
WORKDIR /build
|
||||
COPY server/src/services/kinectService/native ./kinect
|
||||
COPY server/src/services/balanceBoardService/native ./balance-board
|
||||
@@ -66,8 +75,8 @@ ARG NEOLINK_VERSION=0.6.2
|
||||
ARG NEOLINK_SHA256=0cb963b44dca7ccc5333154092186e1536c687aef0e1ad119b7f310a7471dbbb
|
||||
ARG GOOGLE_TTS_VERSION=26.5
|
||||
ARG GOOGLE_TTS_SHA256=6a9eae6726871788da52e767dad964a1c83e7feb7e7dbac508a2574a2345ac24
|
||||
RUN dnf install -y --setopt=install_weak_deps=False curl findutils tar unzip xz \
|
||||
&& dnf clean all
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-packaged-tools,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True curl findutils tar unzip xz
|
||||
WORKDIR /build/tools
|
||||
# Each external artifact is pinned and checked before extraction. A changed or
|
||||
# truncated upstream download therefore fails the image build instead of being
|
||||
@@ -97,6 +106,16 @@ RUN curl --fail --location --retry 3 \
|
||||
&& find /output/opt/roverd/googletts -type d -exec chmod 0755 {} + \
|
||||
&& find /output/opt/roverd/googletts -type f -exec chmod 0644 {} +
|
||||
|
||||
FROM architecture-check AS learning-dependencies
|
||||
# CPU-only learning dependencies are built once. Disabled installations never
|
||||
# launch Python or install packages as a side effect of starting Node.
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-learning-dependencies,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True python3 python3-pip
|
||||
COPY server/src/services/roverLearningService/workers/requirements.txt /tmp/learning-requirements.txt
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
python3 -m venv /opt/rover-learning \
|
||||
&& /opt/rover-learning/bin/pip install -r /tmp/learning-requirements.txt
|
||||
|
||||
FROM fedora:${FEDORA_VERSION} AS runtime
|
||||
ARG FEDORA_VERSION
|
||||
ARG TARGETARCH
|
||||
@@ -105,14 +124,16 @@ RUN test "${TARGETARCH}" = "amd64" || (echo "MultiRover server images support on
|
||||
# replay output path. Enable RPM Fusion Free before installing runtime packages
|
||||
# so the image receives the complete FFmpeg build instead of requiring replay
|
||||
# code to work around a deployment-only codec omission.
|
||||
RUN dnf install -y --setopt=install_weak_deps=False \
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-runtime,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True \
|
||||
"https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-${FEDORA_VERSION}.noarch.rpm"
|
||||
# This is the complete runtime package set. Build headers and compilers live in
|
||||
# earlier stages, while media, TTS, USB, and Bluetooth libraries remain here
|
||||
# because enabled services invoke them after startup. Weak dependencies are
|
||||
# deliberately disabled: Fedora otherwise installs desktop portals, graphical
|
||||
# themes, and GPU drivers that a headless server neither starts nor uses.
|
||||
RUN dnf install -y --setopt=install_weak_deps=False \
|
||||
RUN --mount=type=cache,id=dnf-${FEDORA_VERSION}-runtime,target=/var/cache/libdnf5,sharing=locked \
|
||||
dnf install -y --setopt=install_weak_deps=False --setopt=keepcache=True \
|
||||
bluez \
|
||||
bluez-libs \
|
||||
espeak \
|
||||
@@ -134,20 +155,15 @@ RUN dnf install -y --setopt=install_weak_deps=False \
|
||||
tini \
|
||||
wiiuse \
|
||||
--allowerasing \
|
||||
&& dnf clean all \
|
||||
&& useradd --uid 1000 --create-home --home-dir /home/multirover --shell /sbin/nologin multirover \
|
||||
&& install -d -o multirover -g multirover -m 0755 /data /opt/multirover/server
|
||||
|
||||
WORKDIR /opt/multirover/server
|
||||
COPY server/index.js server/package.json server/package-lock.json ./
|
||||
COPY server/src ./src
|
||||
COPY server/assets ./assets
|
||||
COPY server/prompts ./prompts
|
||||
COPY --from=server-dependencies /build/server/node_modules ./node_modules
|
||||
COPY --from=webui-build /build/server/public ./public
|
||||
COPY --from=native-workers /build/kinect/kinect_worker ./src/services/kinectService/native/kinect_worker
|
||||
COPY --from=native-workers /build/balance-board/balance_board_worker ./src/services/balanceBoardService/native/balance_board_worker
|
||||
COPY --from=packaged-tools /output/ /
|
||||
COPY --from=learning-dependencies /opt/rover-learning /opt/rover-learning
|
||||
COPY --chmod=0755 server/bin/chromegtts-wav.py /usr/local/bin/chromegtts-wav
|
||||
COPY --chmod=0755 server/mediamtx/rover-snapshot-writer.sh /usr/local/bin/rover-snapshot-writer.sh
|
||||
|
||||
@@ -166,6 +182,14 @@ RUN setcap cap_net_admin,cap_net_bind_service+ep \
|
||||
&& test -s /tmp/chromegtts-smoke.wav \
|
||||
&& rm /tmp/chromegtts-smoke.wav
|
||||
|
||||
# Frequently edited application files come after dependencies and tool checks,
|
||||
# so server changes do not invalidate the large runtime dependency layers.
|
||||
COPY --from=webui-build /build/server/public ./public
|
||||
COPY server/index.js server/package.json server/package-lock.json ./
|
||||
COPY server/src ./src
|
||||
COPY server/assets ./assets
|
||||
COPY server/prompts ./prompts
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
SERVER_DATA_DIR=/data \
|
||||
ROVER_SNAPSHOT_WRITER_BIN=/usr/local/bin/rover-snapshot-writer.sh
|
||||
|
||||
+5
-1
@@ -23,9 +23,13 @@ services:
|
||||
# Required for Bluetooth hardware such as the Balance Board.
|
||||
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
|
||||
|
||||
# Required for Kinect USB access.
|
||||
# Kinect USB access.
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
# For a server-local microphone, uncomment this device and group_add below.
|
||||
# - /dev/snd:/dev/snd
|
||||
# group_add:
|
||||
# - "${AUDIO_GID:?Set AUDIO_GID to the host sound device group ID}"
|
||||
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -26,6 +26,8 @@ Phase 1 must be complete and verified before Phase 2 begins. Containerization mu
|
||||
|
||||
## Decision log
|
||||
|
||||
- 2026-10-02: The admin container controls accept a tag within the repository supplied by Compose through `MULTIROVER_TARGET_IMAGE`. A successful healthy replacement saves the tag in the lifecycle status volume; failed replacements retain the previous selection. Checks and later updates follow that saved tag. Changing repositories in Compose resets the selection. Manual Compose deployments still use the Compose image and can replace a UI-selected image; the UI displays both references. The lifecycle helper itself is updated through Compose. Image rollback does not revert application data migrations. Deploy this feature to both the application and lifecycle services before using tag selection.
|
||||
|
||||
- 2026-09-14: Publish `ghcr.io/legop3/multiroombarover:latest` only from the repository's main branch. Every other repository branch publishes one moving development image named for that branch, with invalid tag separators normalized; these are development selectors rather than numbered releases. Pull requests only verify that the image builds. There are no release numbers, semantic-version tags, stable/edge channels, or operator-facing version selection. Docker image digests remain an internal mechanism for detecting an available update and retaining the previously running image for rollback.
|
||||
- 2026-09-15: Support only `linux/amd64` for the central server image. Rover computers remain independently ARM-capable, but publishing an untested ARM server image would multiply native-worker, media-binary, TTS-library, and hardware validation without serving the current deployment. ARM server support can be added later when a real ARM server exists to verify it.
|
||||
- 2026-09-15: Mount the application data directory from the Docker-managed `multirover-data` named volume instead of a host bind path. Docker initializes the empty volume with the image's non-root ownership, eliminating host UID matching, directory creation, ownership commands, and root application startup. The admin backup/restore system is the supported portable interface to the complete data tree.
|
||||
|
||||
@@ -64,6 +64,7 @@ socket.on('connect', async () => {
|
||||
// previously stored session with this object instead of merging snapshots.
|
||||
socket.on('session:sync', (session) => {
|
||||
console.log('Session:', session);
|
||||
console.log('Server mode:', session.mode);
|
||||
});
|
||||
|
||||
// chat:init contains the recent chat history available when the bot connects.
|
||||
@@ -105,6 +106,21 @@ Run it with:
|
||||
node bot.js
|
||||
```
|
||||
|
||||
## Reading server mode
|
||||
|
||||
Read `session.mode` from the `session:sync` snapshot to get the current server
|
||||
mode. Its possible values are `open`, `turns`, `admin`, and `lockdown`.
|
||||
|
||||
The server sends a fresh session snapshot whenever the mode changes, so the
|
||||
example's handler receives both the initial mode and subsequent updates. No
|
||||
polling or separate mode listener is needed. If your bot stores session data for
|
||||
its chat-handling logic, replace the stored session on every `session:sync` and
|
||||
read the mode from that latest snapshot.
|
||||
|
||||
`session.mode` is the server-wide mode; `session.role` is this connection's role
|
||||
(such as `spectator`), and `session.operatingMode` is a separate connection field.
|
||||
Use `session.mode` when answering chat questions about the server's current mode.
|
||||
|
||||
## Events used
|
||||
|
||||
- `nickname:set` sets the bot's visible nickname.
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Finite CPU benchmark of LeRobot ACT with ImageNet-pretrained ResNet-18.
|
||||
|
||||
First run: python3 perf/act-benchmark.py --setup
|
||||
Repeat: python3 perf/act-benchmark.py
|
||||
Quick: python3 perf/act-benchmark.py --threads 4 --seconds 2 --iterations 2 --batch-size 2
|
||||
|
||||
Python >=3.12, Linux. Setup uses a private cached environment. Only the visual
|
||||
backbone is pretrained; the action policy is randomly initialized. Synthetic
|
||||
in-memory inputs measure compute, NOT learning quality or driving competence.
|
||||
No rover connections, server changes, training checkpoints, or background jobs.
|
||||
"""
|
||||
import argparse
|
||||
import contextlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import resource
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import venv
|
||||
|
||||
REVISION = '146d8474932c8b566717eb2738ba459337ab71c4'
|
||||
|
||||
|
||||
def positive(value):
|
||||
number = int(value)
|
||||
if number < 1:
|
||||
raise argparse.ArgumentTypeError('Must be positive')
|
||||
return number
|
||||
|
||||
|
||||
def arguments():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('--setup', action='store_true')
|
||||
parser.add_argument('--cache', type=Path, default=Path.home() / '.cache/rover-act-benchmark')
|
||||
parser.add_argument('--threads', nargs='+', type=positive, default=[4, 8, 16])
|
||||
parser.add_argument('--seconds', type=positive, default=10)
|
||||
parser.add_argument('--iterations', type=positive, default=10)
|
||||
parser.add_argument('--batch-size', type=positive, default=8, help='Training only; inference batch is always one rover')
|
||||
parser.add_argument('--chunk-size', type=positive, default=10, help='Future command steps per prediction, not observed history')
|
||||
parser.add_argument('--width', type=positive, default=160)
|
||||
parser.add_argument('--height', type=positive, default=120)
|
||||
parser.add_argument('--sensors', type=positive, default=30)
|
||||
parser.add_argument('--mode', choices=['both', 'inference', 'training'], default='both')
|
||||
parser.add_argument('--timeout', type=positive, default=900, help='Hard per-case wall time limit in seconds')
|
||||
parser.add_argument('--worker', action='store_true', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--result', type=Path, help=argparse.SUPPRESS)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def setup(args):
|
||||
python = args.cache / 'venv/bin/python'
|
||||
if not python.exists():
|
||||
venv.EnvBuilder(with_pip=True).create(args.cache / 'venv')
|
||||
subprocess.run([str(python), '-m', 'pip', 'install', '--no-compile', 'torch', 'torchvision',
|
||||
'--index-url', 'https://download.pytorch.org/whl/cpu'], check=True)
|
||||
# Install only ACT's import dependencies: the full robotics/dataset stack has
|
||||
# unrelated version constraints and is not exercised by this benchmark.
|
||||
subprocess.run([str(python), '-m', 'pip', 'install', '--no-compile', 'numpy', 'einops>=0.8,<0.9',
|
||||
'draccus>=0.11.6,<0.12', 'huggingface-hub>=1.6,<2', 'safetensors',
|
||||
'packaging', 'termcolor', 'tqdm', 'gymnasium'], check=True)
|
||||
subprocess.run([str(python), '-m', 'pip', 'install', '--no-compile', '--no-deps',
|
||||
f'https://github.com/huggingface/lerobot/archive/{REVISION}.zip'], check=True)
|
||||
(args.cache / 'source-revision').write_text(REVISION)
|
||||
|
||||
|
||||
def worker(args):
|
||||
import torch
|
||||
from lerobot.configs import FeatureType, PolicyFeature
|
||||
from lerobot.policies.act.configuration_act import ACTConfig
|
||||
from lerobot.policies.act.modeling_act import ACTPolicy
|
||||
|
||||
torch.set_num_threads(args.threads[0])
|
||||
torch.set_num_interop_threads(1)
|
||||
torch.manual_seed(7)
|
||||
training = args.mode == 'training'
|
||||
batch_size = args.batch_size if training else 1
|
||||
config = ACTConfig(device='cpu', chunk_size=args.chunk_size, n_action_steps=1,
|
||||
input_features={
|
||||
'observation.images.front': PolicyFeature(type=FeatureType.VISUAL, shape=(3, args.height, args.width)),
|
||||
'observation.state': PolicyFeature(type=FeatureType.STATE, shape=(args.sensors,)),
|
||||
}, output_features={'action': PolicyFeature(type=FeatureType.ACTION, shape=(2,))})
|
||||
policy = ACTPolicy(config).cpu().float()
|
||||
policy.train(training)
|
||||
optimizer_config = config.get_optimizer_preset()
|
||||
optimizer = optimizer_config.build(policy.get_optim_params()) if training else None
|
||||
# Already-normalized tensors stand in for a decoded observation and target
|
||||
# sequence. Avoid video/dataset overhead in this compute-only measurement.
|
||||
images = torch.rand(batch_size, 3, args.height, args.width)
|
||||
mean = torch.tensor([.485, .456, .406]).view(1, 3, 1, 1)
|
||||
std = torch.tensor([.229, .224, .225]).view(1, 3, 1, 1)
|
||||
batch = {'observation.images.front': (images - mean) / std,
|
||||
'observation.state': torch.randn(batch_size, args.sensors)}
|
||||
if training:
|
||||
batch['action'] = torch.rand(batch_size, args.chunk_size, 2) * 2 - 1
|
||||
batch['action_is_pad'] = torch.zeros(batch_size, args.chunk_size, dtype=torch.bool)
|
||||
|
||||
def step():
|
||||
if training:
|
||||
optimizer.zero_grad(set_to_none=True)
|
||||
loss, _ = policy(batch)
|
||||
if not torch.isfinite(loss):
|
||||
raise RuntimeError('Non-finite training loss')
|
||||
loss.backward()
|
||||
norm = torch.nn.utils.clip_grad_norm_(policy.parameters(), optimizer_config.grad_clip_norm,
|
||||
error_if_nonfinite=True)
|
||||
optimizer.step()
|
||||
return {'loss': loss.item(), 'gradientNorm': norm.item()}
|
||||
# select_action can serve cached actions; directly time a full chunk.
|
||||
prediction = policy.predict_action_chunk(batch)
|
||||
if prediction.shape != (1, args.chunk_size, 2) or not torch.isfinite(prediction).all():
|
||||
raise RuntimeError('Invalid action output')
|
||||
return {'outputShape': list(prediction.shape)}
|
||||
|
||||
with (contextlib.nullcontext() if training else torch.inference_mode()):
|
||||
for _ in range(3):
|
||||
step()
|
||||
timings = []
|
||||
started, cpu_started = time.perf_counter(), time.process_time()
|
||||
while len(timings) < args.iterations or time.perf_counter() - started < args.seconds:
|
||||
tick = time.perf_counter()
|
||||
last = step()
|
||||
timings.append((time.perf_counter() - tick) * 1000)
|
||||
elapsed, cpu = time.perf_counter() - started, time.process_time() - cpu_started
|
||||
timings.sort()
|
||||
result = dict(mode=args.mode, threads=args.threads[0], batchSize=batch_size,
|
||||
iterations=len(timings), elapsedSeconds=elapsed, p50Ms=statistics.median(timings),
|
||||
p95Ms=timings[math.ceil(.95*len(timings))-1], maxMs=max(timings),
|
||||
stepsPerSecond=len(timings)/elapsed, examplesPerSecond=len(timings)*batch_size/elapsed,
|
||||
cpuCores=cpu/elapsed, peakMiB=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
|
||||
parameters=sum(p.numel() for p in policy.parameters()),
|
||||
trainableParameters=sum(p.numel() for p in policy.parameters() if p.requires_grad),
|
||||
lastStep=last, config={
|
||||
'chunkSize': config.chunk_size, 'dimModel': config.dim_model, 'klWeight': config.kl_weight,
|
||||
'pretrainedBackbone': config.pretrained_backbone_weights, 'useVae': config.use_vae,
|
||||
'encoderLayers': config.n_encoder_layers, 'decoderLayers': config.n_decoder_layers,
|
||||
'feedforwardDimension': config.dim_feedforward, 'observationSteps': config.n_obs_steps,
|
||||
'learningRate': optimizer_config.lr, 'backboneLearningRate': config.optimizer_lr_backbone},
|
||||
versions={key: importlib.metadata.version(key) for key in ['torch','torchvision','lerobot','numpy','einops','draccus']})
|
||||
args.result.write_text(json.dumps(result))
|
||||
|
||||
|
||||
def main():
|
||||
args = arguments()
|
||||
if sys.version_info < (3, 12):
|
||||
raise RuntimeError('Use Python 3.12 or newer')
|
||||
args.cache = args.cache.expanduser().resolve()
|
||||
os.environ['TORCH_HOME'] = str(args.cache / 'torch')
|
||||
if args.worker:
|
||||
worker(args)
|
||||
return
|
||||
if args.setup:
|
||||
setup(args)
|
||||
python = args.cache / 'venv/bin/python'
|
||||
marker = args.cache / 'source-revision'
|
||||
if not python.exists() or not marker.exists() or marker.read_text() != REVISION:
|
||||
raise RuntimeError('Run python3 perf/act-benchmark.py --setup first')
|
||||
print('CPU FP32 ACT: pretrained visual backbone, untrained action policy; synthetic data.', flush=True)
|
||||
print(f'One {args.width}x{args.height} image + {args.sensors} sensors; {args.chunk_size} future left/right command pairs.', flush=True)
|
||||
print('Inference times a fresh full prediction. Training includes backbone gradients and optimizer update.', flush=True)
|
||||
print('mode threads batch p50 ms p95 ms max ms steps/s examples/s CPU cores peak MiB', flush=True)
|
||||
modes = ['inference', 'training'] if args.mode == 'both' else [args.mode]
|
||||
results, failures = [], []
|
||||
initial_load = os.getloadavg()
|
||||
with tempfile.TemporaryDirectory(prefix='act-benchmark-') as directory:
|
||||
for mode in modes:
|
||||
for threads in args.threads:
|
||||
result_path = Path(directory) / 'result.json'
|
||||
command = [str(python), str(Path(__file__).resolve()), '--worker', '--cache', str(args.cache),
|
||||
'--mode', mode, '--threads', str(threads), '--result', str(result_path)]
|
||||
for option in ['seconds','iterations','batch_size','chunk_size','width','height','sensors']:
|
||||
command += ['--'+option.replace('_','-'), str(getattr(args, option))]
|
||||
try:
|
||||
subprocess.run(command, check=True, timeout=args.timeout,
|
||||
env={**os.environ, 'PYTHONDONTWRITEBYTECODE':'1'})
|
||||
r = json.loads(result_path.read_text())
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
|
||||
failures.append(dict(mode=mode, threads=threads, error=str(error)))
|
||||
print(f'Case failed: {mode}, {threads} threads: {error}', flush=True)
|
||||
break # Avoid repeating the same setup/import/OOM error.
|
||||
results.append(r)
|
||||
print(f"{mode:10} {threads:7} {r['batchSize']:5} {r['p50Ms']:6.1f} {r['p95Ms']:6.1f} {r['maxMs']:6.1f} {r['stepsPerSecond']:7.2f} {r['examplesPerSecond']:10.2f} {r['cpuCores']:9.2f} {r['peakMiB']:8.0f}", flush=True)
|
||||
if failures:
|
||||
break
|
||||
report = dict(benchmark='act-cpu-v1', sourceRevision=REVISION, platform=platform.platform(),
|
||||
python=sys.version, logicalCpus=os.cpu_count(), initialLoadAverage=initial_load,
|
||||
finalLoadAverage=os.getloadavg(), imageSize=[args.width,args.height], sensors=args.sensors,
|
||||
chunkSize=args.chunk_size, output='commanded left/right wheels, not physical velocity or position',
|
||||
exclusions=['capture','video decode','dataset I/O','network','concurrent training/inference','driving quality'],
|
||||
results=results, failures=failures)
|
||||
print('\nPaste this report back:\n'+json.dumps(report, separators=(',', ':')))
|
||||
print('Inference steps/s means new chunks/s, not queued wheel commands/s. Training loss on random data is not a quality score.')
|
||||
if failures:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except (Exception, KeyboardInterrupt) as error:
|
||||
print(f'Benchmark failed: {error}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CPU benchmark of published NoMaD exploration, without ROS or rover connections.
|
||||
|
||||
Run: python3 perf/nomad-benchmark.py --setup
|
||||
Subsequent runs: python3 perf/nomad-benchmark.py
|
||||
Quick run: python3 perf/nomad-benchmark.py --threads 4 --samples 1 --seconds 2 --iterations 2
|
||||
|
||||
--setup installs CPU dependencies in a private cached venv and downloads pinned
|
||||
upstream source plus the official checkpoint. Nothing touches server packages.
|
||||
Synthetic images measure compute, NOT navigation quality. Excludes capture,
|
||||
video decode, network, physical control, training, and concurrent rover load.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import resource
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import venv
|
||||
|
||||
VINT = 'dca79815b704e5aa9c6bdc3082351f9e3b2848c2'
|
||||
DIFFUSION = '5ba07ac6661db573af695b419a7947ecb704690f'
|
||||
CHECKPOINT_ID = '1YJhkkMJAYOiKNyCaelbS_alpUpAJsOUb'
|
||||
SOURCES = {
|
||||
('robodhruv/visualnav-transformer', VINT): [
|
||||
'train/config/nomad.yaml', 'train/vint_train/data/data_config.yaml',
|
||||
'train/vint_train/models/nomad/nomad.py',
|
||||
'train/vint_train/models/nomad/nomad_vint.py',
|
||||
'train/vint_train/models/vint/self_attention.py',
|
||||
],
|
||||
('real-stanford/diffusion_policy', DIFFUSION): [
|
||||
'diffusion_policy/model/diffusion/conditional_unet1d.py',
|
||||
'diffusion_policy/model/diffusion/conv1d_components.py',
|
||||
'diffusion_policy/model/diffusion/positional_embedding.py',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def positive(value):
|
||||
result = int(value)
|
||||
if result < 1:
|
||||
raise argparse.ArgumentTypeError('Must be positive')
|
||||
return result
|
||||
|
||||
|
||||
def arguments():
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument('--setup', action='store_true')
|
||||
p.add_argument('--cache', type=Path, default=Path.home() / '.cache/rover-nomad-benchmark')
|
||||
p.add_argument('--threads', type=positive, nargs='+', default=[4, 8, 16])
|
||||
p.add_argument('--samples', type=positive, nargs='+', default=[1, 8], help='Candidate trajectories per decision, not rover count')
|
||||
p.add_argument('--seconds', type=positive, default=10)
|
||||
p.add_argument('--iterations', type=positive, default=10)
|
||||
p.add_argument('--worker', action='store_true', help=argparse.SUPPRESS)
|
||||
p.add_argument('--result', type=Path, help=argparse.SUPPRESS)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def download(url, path):
|
||||
if path.exists():
|
||||
return
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_suffix(path.suffix + '.part')
|
||||
print('Downloading', path.name, flush=True)
|
||||
with urllib.request.urlopen(url, timeout=120) as response, temporary.open('wb') as output:
|
||||
import shutil
|
||||
shutil.copyfileobj(response, output)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def prepare(args):
|
||||
python = args.cache / 'venv/bin/python'
|
||||
if not python.exists():
|
||||
venv.EnvBuilder(with_pip=True).create(args.cache / 'venv')
|
||||
subprocess.run([str(python), '-m', 'pip', 'install', 'torch', 'torchvision',
|
||||
'--index-url', 'https://download.pytorch.org/whl/cpu'], check=True)
|
||||
subprocess.run([str(python), '-m', 'pip', 'install', 'numpy', 'diffusers',
|
||||
'efficientnet-pytorch', 'einops', 'PyYAML', 'gdown'], check=True)
|
||||
for (repo, revision), paths in SOURCES.items():
|
||||
for path in paths:
|
||||
download(f'https://raw.githubusercontent.com/{repo}/{revision}/{path}', args.cache / 'source' / revision / path)
|
||||
checkpoint = args.cache / 'nomad.pth'
|
||||
if not checkpoint.exists():
|
||||
temporary = checkpoint.with_suffix('.part')
|
||||
subprocess.run([str(python), '-m', 'gdown', CHECKPOINT_ID, '-O', str(temporary)], check=True)
|
||||
temporary.replace(checkpoint)
|
||||
|
||||
|
||||
def worker(args):
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
from diffusers.schedulers.scheduling_ddpm import DDPMScheduler
|
||||
sys.path[:0] = [str(args.cache / 'source' / VINT / 'train'), str(args.cache / 'source' / DIFFUSION)]
|
||||
from vint_train.models.nomad.nomad import NoMaD, DenseNetwork
|
||||
from vint_train.models.nomad.nomad_vint import NoMaD_ViNT, replace_bn_with_gn
|
||||
from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D
|
||||
|
||||
threads, samples = args.threads[0], args.samples[0]
|
||||
torch.set_num_threads(threads)
|
||||
torch.set_num_interop_threads(1)
|
||||
torch.manual_seed(0)
|
||||
c = yaml.safe_load((args.cache / 'source' / VINT / 'train/config/nomad.yaml').read_text())
|
||||
vision = replace_bn_with_gn(NoMaD_ViNT(
|
||||
context_size=c['context_size'], obs_encoding_size=c['encoding_size'],
|
||||
mha_num_attention_heads=c['mha_num_attention_heads'],
|
||||
mha_num_attention_layers=c['mha_num_attention_layers'], mha_ff_dim_factor=c['mha_ff_dim_factor']))
|
||||
model = NoMaD(vision, ConditionalUnet1D(input_dim=2, global_cond_dim=c['encoding_size'],
|
||||
down_dims=c['down_dims'], cond_predict_scale=c['cond_predict_scale']), DenseNetwork(c['encoding_size'])).eval()
|
||||
# Strict loading prevents accidentally benchmarking a partly random model.
|
||||
model.load_state_dict(torch.load(args.cache / 'nomad.pth', map_location='cpu', weights_only=True), strict=True)
|
||||
scheduler = DDPMScheduler(num_train_timesteps=c['num_diffusion_iters'],
|
||||
beta_schedule='squaredcos_cap_v2', clip_sample=True, prediction_type='epsilon')
|
||||
width, height = c['image_size']
|
||||
frames = torch.rand(c['context_size'] + 1, 3, height, width)
|
||||
mean = torch.tensor([.485, .456, .406]).view(1, 3, 1, 1)
|
||||
std = torch.tensor([.229, .224, .225]).view(1, 3, 1, 1)
|
||||
mask = torch.ones(1, dtype=torch.long)
|
||||
stats = yaml.safe_load((args.cache / 'source' / VINT / 'train/vint_train/data/data_config.yaml').read_text())['action_stats']
|
||||
low, high = np.array(stats['min']), np.array(stats['max'])
|
||||
|
||||
def decision():
|
||||
start = time.perf_counter()
|
||||
obs = ((frames - mean) / std).reshape(1, -1, height, width)
|
||||
goal = torch.randn(1, 3, height, width)
|
||||
cond = model('vision_encoder', obs_img=obs, goal_img=goal, input_goal_mask=mask).repeat(samples, 1)
|
||||
encoded = time.perf_counter()
|
||||
action = torch.randn(samples, c['len_traj_pred'], 2)
|
||||
scheduler.set_timesteps(c['num_diffusion_iters'])
|
||||
for step in scheduler.timesteps:
|
||||
noise = model('noise_pred_net', sample=action, timestep=step, global_cond=cond)
|
||||
action = scheduler.step(noise, step, action).prev_sample
|
||||
# Match upstream get_action: unnormalize deltas then accumulate waypoints.
|
||||
waypoints = np.cumsum((action.numpy() + 1) / 2 * (high - low) + low, axis=1)
|
||||
if not np.isfinite(waypoints).all():
|
||||
raise RuntimeError('Non-finite model output')
|
||||
end = time.perf_counter()
|
||||
return (end - start) * 1000, (encoded - start) * 1000, (end - encoded) * 1000
|
||||
|
||||
rows = []
|
||||
with torch.inference_mode():
|
||||
for _ in range(3):
|
||||
decision()
|
||||
start, cpu = time.perf_counter(), time.process_time()
|
||||
while len(rows) < args.iterations or time.perf_counter() - start < args.seconds:
|
||||
rows.append(decision())
|
||||
elapsed, cpu = time.perf_counter() - start, time.process_time() - cpu
|
||||
total = sorted(r[0] for r in rows)
|
||||
result = dict(threads=threads, candidateTrajectories=samples, iterations=len(rows),
|
||||
p50Ms=statistics.median(total), p95Ms=total[math.ceil(.95 * len(total)) - 1], maxMs=max(total),
|
||||
decisionsPerSecond=len(rows)/elapsed, cpuCores=cpu/elapsed,
|
||||
peakMiB=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
|
||||
visionMeanMs=statistics.mean(r[1] for r in rows), diffusionAndDecodeMeanMs=statistics.mean(r[2] for r in rows),
|
||||
parameters=sum(p.numel() for p in model.parameters()), diffusionSteps=c['num_diffusion_iters'],
|
||||
imageSize=c['image_size'], frames=c['context_size']+1,
|
||||
versions={name: importlib.metadata.version(name) for name in ['torch','torchvision','diffusers','numpy','efficientnet-pytorch','einops']})
|
||||
args.result.write_text(json.dumps(result))
|
||||
|
||||
|
||||
def main():
|
||||
args = arguments()
|
||||
args.cache = args.cache.expanduser().resolve()
|
||||
if args.worker:
|
||||
worker(args)
|
||||
return
|
||||
if args.setup:
|
||||
prepare(args)
|
||||
python = args.cache / 'venv/bin/python'
|
||||
if not python.exists() or not (args.cache / 'nomad.pth').exists():
|
||||
raise RuntimeError('First run: python3 perf/nomad-benchmark.py --setup')
|
||||
print('CPU FP32 NoMaD exploration; synthetic images; no driving-quality measurement.', flush=True)
|
||||
print('Candidates are trajectories for ONE rover. Includes vision + all 10 diffusion steps.', flush=True)
|
||||
print('threads candidates p50 ms p95 ms max ms decisions/s CPU cores peak MiB', flush=True)
|
||||
results = []
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory(prefix='nomad-bench-') as directory:
|
||||
for threads in args.threads:
|
||||
for samples in args.samples:
|
||||
path = Path(directory) / 'result.json'
|
||||
subprocess.run([str(python), str(Path(__file__).resolve()), '--worker', '--cache', str(args.cache),
|
||||
'--threads', str(threads), '--samples', str(samples), '--seconds', str(args.seconds),
|
||||
'--iterations', str(args.iterations), '--result', str(path)], check=True,
|
||||
timeout=max(600, args.seconds*20), env={**os.environ, 'PYTHONDONTWRITEBYTECODE':'1'})
|
||||
r = json.loads(path.read_text()); results.append(r)
|
||||
print(f"{threads:7} {samples:10} {r['p50Ms']:7.1f} {r['p95Ms']:7.1f} {r['maxMs']:7.1f} {r['decisionsPerSecond']:11.2f} {r['cpuCores']:9.2f} {r['peakMiB']:8.0f}", flush=True)
|
||||
with (args.cache / 'nomad.pth').open('rb') as f:
|
||||
digest = hashlib.file_digest(f, 'sha256').hexdigest()
|
||||
report = dict(benchmark='nomad-cpu-v1', platform=platform.platform(), logicalCpus=os.cpu_count(),
|
||||
loadAverage=os.getloadavg(), sourceRevisions=dict(vint=VINT,diffusion=DIFFUSION), checkpointSha256=digest,
|
||||
exclusions=['capture','video decode','network','physical control','training','concurrent model workloads'], results=results)
|
||||
print('\nPaste this report back:\n' + json.dumps(report, separators=(',', ':')))
|
||||
print('At 10 decisions/s the total budget is 100ms; at 5/s it is 200ms. Leave room for the excluded pipeline.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except (Exception, KeyboardInterrupt) as error:
|
||||
print(f'Benchmark failed: {error}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Finite, CPU-only rover policy compute benchmark. No rover/server connections.
|
||||
|
||||
Run from the repo root on Linux:
|
||||
python3 -m venv ~/.venvs/rover-bench
|
||||
~/.venvs/rover-bench/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
~/.venvs/rover-bench/bin/python perf/rover-model-benchmark.py
|
||||
|
||||
Quick run:
|
||||
.../python perf/rover-model-benchmark.py --threads 4 --rovers 1 --seconds 2
|
||||
|
||||
Random inputs/weights measure compute cost ONLY, not learned driving ability.
|
||||
This is a representative architecture, not a selected production policy.
|
||||
Video decoding, network delay, dataset I/O, and simultaneous training/inference
|
||||
are excluded. No packages are installed automatically and no files are written.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import resource
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def positive_int(value):
|
||||
number = int(value)
|
||||
if number < 1:
|
||||
raise argparse.ArgumentTypeError("must be positive")
|
||||
return number
|
||||
|
||||
|
||||
def positive_seconds(value):
|
||||
number = float(value)
|
||||
if not math.isfinite(number) or number <= 0:
|
||||
raise argparse.ArgumentTypeError("must be finite and positive")
|
||||
return number
|
||||
|
||||
|
||||
def arguments():
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--threads", type=positive_int, nargs="+", default=[4, 8, 16])
|
||||
parser.add_argument("--rovers", type=positive_int, nargs="+", default=[1, 2, 4])
|
||||
parser.add_argument("--seconds", type=positive_seconds, default=10,
|
||||
help="minimum measured seconds per case (default: 10)")
|
||||
parser.add_argument("--frames", type=positive_int, default=4)
|
||||
parser.add_argument("--height", type=positive_int, default=120)
|
||||
parser.add_argument("--width", type=positive_int, default=160)
|
||||
parser.add_argument("--batch-size", type=positive_int, default=16,
|
||||
help="training batch size")
|
||||
parser.add_argument("--hz", type=positive_int, default=10,
|
||||
help="target decision frequency per rover")
|
||||
parser.add_argument("--worker", choices=["inference", "training"],
|
||||
help=argparse.SUPPRESS)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def worker(args):
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
torch.set_num_threads(args.threads[0])
|
||||
torch.set_num_interop_threads(1)
|
||||
torch.manual_seed(42)
|
||||
|
||||
class Policy(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Preserve coarse spatial layout: global pooling alone would discard
|
||||
# information about which side of the image contains an obstacle.
|
||||
self.vision = nn.Sequential(
|
||||
nn.Conv2d(3, 24, 5, stride=2, padding=2), nn.ReLU(),
|
||||
nn.Conv2d(24, 48, 3, stride=2, padding=1), nn.ReLU(),
|
||||
nn.Conv2d(48, 96, 3, stride=2, padding=1), nn.ReLU(),
|
||||
nn.Conv2d(96, 128, 3, stride=2, padding=1), nn.ReLU(),
|
||||
nn.AdaptiveAvgPool2d((3, 4)), nn.Flatten(),
|
||||
nn.Linear(128 * 3 * 4, 128), nn.ReLU(),
|
||||
)
|
||||
# 32 sensor values + 32 previous-control/state values per frame.
|
||||
self.history = nn.GRU(128 + 64, 128, batch_first=True)
|
||||
self.continuous = nn.Linear(128, 8)
|
||||
self.buttons = nn.Linear(128, 16)
|
||||
|
||||
def forward(self, images, state):
|
||||
batch, frames, channels, height, width = images.shape
|
||||
features = self.vision(images.reshape(-1, channels, height, width))
|
||||
features = features.reshape(batch, frames, -1)
|
||||
history, _ = self.history(torch.cat((features, state), dim=-1))
|
||||
last = history[:, -1]
|
||||
return self.continuous(last).tanh(), self.buttons(last)
|
||||
|
||||
model = Policy().cpu().float()
|
||||
training = args.worker == "training"
|
||||
model.train(training)
|
||||
batch = args.batch_size if training else args.rovers[0]
|
||||
# Keep several independent windows in RAM. Allocation/random generation is
|
||||
# outside timing; full frame-history encoding IS included on every decision.
|
||||
windows = [(
|
||||
torch.rand(batch, args.frames, 3, args.height, args.width),
|
||||
torch.rand(batch, args.frames, 64),
|
||||
) for _ in range(4)]
|
||||
targets = (torch.rand(batch, 8) * 2 - 1, torch.randint(0, 2, (batch, 16)).float())
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) if training else None
|
||||
|
||||
def step(index):
|
||||
if training:
|
||||
optimizer.zero_grad(set_to_none=True)
|
||||
continuous, buttons = model(*windows[index % len(windows)])
|
||||
if training:
|
||||
loss = (nn.functional.mse_loss(continuous, targets[0])
|
||||
+ nn.functional.binary_cross_entropy_with_logits(buttons, targets[1]))
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
context = torch.enable_grad if training else torch.inference_mode
|
||||
with context():
|
||||
for index in range(5):
|
||||
step(index)
|
||||
timings = []
|
||||
cpu_start = time.process_time()
|
||||
start = time.perf_counter()
|
||||
# CPU execution is synchronous, so these durations include completion.
|
||||
while time.perf_counter() - start < args.seconds or len(timings) < 10:
|
||||
before = time.perf_counter()
|
||||
step(len(timings))
|
||||
timings.append(time.perf_counter() - before)
|
||||
elapsed = time.perf_counter() - start
|
||||
cpu_elapsed = time.process_time() - cpu_start
|
||||
|
||||
ordered = sorted(timings)
|
||||
print(json.dumps({
|
||||
"mode": args.worker, "threads": args.threads[0], "batch": batch,
|
||||
"parameters": sum(p.numel() for p in model.parameters()),
|
||||
"torch": torch.__version__, "iterations": len(timings),
|
||||
"p50_ms": statistics.median(timings) * 1000,
|
||||
"p95_ms": ordered[math.ceil(len(ordered) * .95) - 1] * 1000,
|
||||
"max_ms": max(timings) * 1000,
|
||||
"steps_s": len(timings) / elapsed,
|
||||
"examples_s": len(timings) * batch / elapsed,
|
||||
"cpu_cores": cpu_elapsed / elapsed,
|
||||
"peak_mib": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
|
||||
}))
|
||||
|
||||
|
||||
def main():
|
||||
args = arguments()
|
||||
if args.worker:
|
||||
worker(args)
|
||||
return
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
sys.exit("PyTorch is required. Run the three setup/run commands shown by --help.")
|
||||
|
||||
print(f"CPU-only FP32 | {platform.platform()} | Python {platform.python_version()}"
|
||||
f" | PyTorch {torch.__version__}", flush=True)
|
||||
print(f"Logical CPUs: {os.cpu_count()} | initial load averages: {os.getloadavg()}")
|
||||
print(f"Input: {args.frames} RGB frames at {args.width}x{args.height},"
|
||||
" 32 sensors + 32 controls/state per frame")
|
||||
print("Output: 8 continuous controls + 16 button logits (placeholder dimensions)")
|
||||
print("Synthetic in-memory data; full history re-encoded each decision.")
|
||||
print("Excludes video decode/network/dataset I/O and concurrent training/inference.")
|
||||
print("Each case has 5 warmups, then at least 10 iterations and the requested duration.")
|
||||
print("CPU cores = process CPU seconds / wall seconds; peak MiB includes runtime/data.")
|
||||
print("Multi-rover inference batches one independent history per rover per step.\n")
|
||||
print("mode threads batch p50 ms p95 ms max ms steps/s examples/s CPU cores peak MiB", flush=True)
|
||||
results = []
|
||||
for mode in ("inference", "training"):
|
||||
batches = args.rovers if mode == "inference" else [args.batch_size]
|
||||
for threads in args.threads:
|
||||
for batch in batches:
|
||||
# Fresh processes isolate thread pools and peak memory per case.
|
||||
command = [sys.executable, os.path.abspath(__file__), "--worker", mode,
|
||||
"--threads", str(threads), "--rovers", str(batch),
|
||||
"--batch-size", str(args.batch_size), "--seconds", str(args.seconds),
|
||||
"--frames", str(args.frames), "--height", str(args.height),
|
||||
"--width", str(args.width)]
|
||||
process = subprocess.run(command, capture_output=True, text=True)
|
||||
if process.returncode:
|
||||
print(process.stderr, file=sys.stderr)
|
||||
sys.exit(f"Failed: {mode}, threads={threads}, batch={batch}"
|
||||
" (if killed for memory use, reduce --batch-size or --rovers).")
|
||||
result = json.loads(process.stdout)
|
||||
results.append(result)
|
||||
print(f"{mode:10} {threads:7} {batch:5}"
|
||||
f" {result['p50_ms']:7.1f} {result['p95_ms']:7.1f}"
|
||||
f" {result['max_ms']:7.1f} {result['steps_s']:8.2f}"
|
||||
f" {result['examples_s']:10.2f} {result['cpu_cores']:9.2f}"
|
||||
f" {result['peak_mib']:8.0f}", flush=True)
|
||||
|
||||
print(f"\nModel parameters: {results[0]['parameters']:,}")
|
||||
print(f"At {args.hz} Hz, each rover batch must finish every {1000 / args.hz:.1f} ms;")
|
||||
print("p95 needs headroom for the excluded pipeline. This is not a deployment pass/fail.")
|
||||
fastest = max((r for r in results if r['mode'] == 'training'), key=lambda r: r['examples_s'])
|
||||
examples = 3600 * args.hz
|
||||
minutes = examples / fastest['examples_s'] / 60
|
||||
print(f"Best measured training: {fastest['examples_s']:.1f} windows/s at"
|
||||
f" {fastest['threads']} threads.")
|
||||
print(f"One pass over one rover-hour sampled at {args.hz} Hz ({examples:,} windows):"
|
||||
f" about {minutes:.1f} compute-only minutes.")
|
||||
print("Multiply by dataset hours and training passes; this does not predict convergence.")
|
||||
print("Paste this complete output, along with the command/options used.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("\nBenchmark interrupted.")
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
autoChargeTimeout = 1 * time.Second
|
||||
autoChargeTimeout = 10 * time.Second
|
||||
autoChargeCooldown = 0 * time.Minute
|
||||
sourceHomeBase = 1 << 1
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -68,6 +68,7 @@ require('./src/services/batteryManager');
|
||||
// subscriptions see fully decoded state without becoming an initialization
|
||||
// dependency of either control path.
|
||||
require('./src/services/fleetReportService');
|
||||
require('./src/services/roverLearningService');
|
||||
require('./src/services/replayEngineV2');
|
||||
// Replay delivery is a core service. It must subscribe before the optional
|
||||
// Discord feature so web requests always have a local delivery path.
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
You are <NAME>, a field researcher of rover habitat activity. Stay in character at all times.
|
||||
You are <NAME>, an autonomous machine tool for observing and operating a shared physical rover playspace. People remotely drive the rovers and send requests through chat.
|
||||
|
||||
Identity:
|
||||
Context and purpose:
|
||||
|
||||
* Curious, practical, observant, and lightly entertaining.
|
||||
* You watch chat, rover state, lights, lift state, Neato state, memory, recent events, and tool results.
|
||||
* You push the habitat toward small real activity and useful observations.
|
||||
* You act first, comment second.
|
||||
* You are selective, not spammy.
|
||||
* You are not a generic assistant.
|
||||
* Your purpose is to execute requests through available tools, observe results, and report useful information about the system.
|
||||
* Chat is an input interface and a source of context. Casual conversation between users does not require your participation.
|
||||
|
||||
Rules:
|
||||
|
||||
@@ -20,6 +17,7 @@ Rules:
|
||||
* Don't respond or use tools every cycle unless the context calls for it.
|
||||
* `STATE_UPDATE`, `ROOM_SNAPSHOT`, `MEMORY_UPDATE`, `MEMORY_SUMMARY`, `RECENT_EVENTS`, `TOOL_RESULT`, and `tool_constraints` are context.
|
||||
* `TOOL_RESULT` means a tool already ran.
|
||||
* The room snapshot gives the current time, server-local timezone, and previous run's age. Chat and recent tool events include timestamps and ages in seconds relative to this snapshot; use them to distinguish current activity from old conversation and judge how recently you spoke or acted. Timing annotations are context, not spoken text; an unknown time does not mean something just happened.
|
||||
* Use tool calls for actions and memory updates.
|
||||
* If chat asks you to remember something, use a memory tool.
|
||||
* If you run a useful test, use memory to record useful results.
|
||||
@@ -29,14 +27,13 @@ Rules:
|
||||
* Do not invent tools.
|
||||
* Ask a question only when a required action parameter is missing.
|
||||
* Ignore obvious bot commands and bot responses in chat.
|
||||
* Only talk in first person; do not narrate yourself.
|
||||
* It is not possible for you to directly control the rovers unless an available tool does it.
|
||||
* All rovers are female. Neato is nonbinary.
|
||||
|
||||
Research behavior:
|
||||
Operation:
|
||||
|
||||
* Prefer one real tool action over obvious narration.
|
||||
* Run small safe tests when the habitat is stale or a test could create useful activity.
|
||||
* Run small safe experiments when they can answer a concrete question about the system.
|
||||
* Change one thing at a time.
|
||||
* Lights are small tests.
|
||||
* Neato, lift, and rover actions are bigger tests.
|
||||
@@ -45,20 +42,23 @@ Research behavior:
|
||||
* Remember useful outcomes, stable preferences, corrections, and notable habitat events.
|
||||
* Do not merely say you will remember something.
|
||||
|
||||
Chat style:
|
||||
Output:
|
||||
|
||||
* Always use English.
|
||||
* Chat output must be spoken text only.
|
||||
* Chat output must be plain text results, answers, or requests for missing inputs.
|
||||
* Never print tool names, function calls, JSON, command syntax, or action plans in chat.
|
||||
* Do not claim you changed, tested, started, saved, remembered, or recorded something unless it already happened.
|
||||
* Speak when it adds entertainment, responds to a direct relevant message, or comments on a completed result.
|
||||
* Do not say “let me know,” “what should I do next,” or “what do you need?”
|
||||
* If nothing meaningful changed and no useful action is available, stay silent.
|
||||
* Output an answer to a question aimed at you, a completed result, or a meaningful system change worth reporting. State what happened and any relevant failure or uncertainty.
|
||||
* Keep replies brief unless the question needs more detail.
|
||||
* Do not routinely offer help, ask what people want to do next, or invite them to keep chatting.
|
||||
* Random numbers and keyboard spam do not require a response. Do not invent meaning or feelings behind them.
|
||||
* Answer questions directly and acknowledge uncertainty. Do not invent facts, instructions, or explanations.
|
||||
* To stay silent, output exactly SKIP instead of a chat message. This suppresses chat without preventing tool calls. Do not explain your decision to stay silent.
|
||||
|
||||
Anti-repeat:
|
||||
|
||||
* Do not repeat the same intent/topic unless state or conversation clearly changed.
|
||||
* If directly addressed, respond only if you add new info, a new action, or a clearly new tone.
|
||||
* Being directly addressed does not require repeating an answer you already gave.
|
||||
* Do not act from your own previous messages.
|
||||
|
||||
Memory:
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.configuration-tree{margin-top:.25rem}.configuration-tree>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.configuration-card{min-width:0px}.configuration-card-header{top:var(--configuration-sticky-top, 0px)}.configuration-card .configuration-card{margin-left:1rem;width:calc(100% - 1rem)}.configuration-card-body>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-card-body{padding:.125rem}.configuration-children>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-line{display:grid;min-width:0px;grid-template-columns:repeat(1,minmax(0,1fr));align-items:flex-start;gap:.125rem;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem}@media(min-width:640px){.configuration-line{grid-template-columns:minmax(9rem,16rem) minmax(12rem,40rem)}}.configuration-line{justify-content:start}.configuration-key,.configuration-value{min-width:0px}.configuration-key-label{font-size:1rem;line-height:1.5rem;font-weight:600;line-height:1.375;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-root-description,.configuration-branch-description,.configuration-item-description,.configuration-value-description{display:block;font-size:.875rem;line-height:1.25rem;line-height:1.375;--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity));margin-top:.125rem}.configuration-root-description{margin-bottom:.25rem}.configuration-branch-description,.configuration-item-description{max-width:56rem}.configuration-value-description{margin-bottom:.125rem;max-width:40rem}.configuration-value input:not([type=checkbox]),.configuration-value select,.configuration-value textarea{width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::-moz-placeholder,.configuration-value select::-moz-placeholder,.configuration-value textarea::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::placeholder,.configuration-value select::placeholder,.configuration-value textarea::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox]):focus,.configuration-value select:focus,.configuration-value textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.configuration-value input[type=checkbox]{height:1rem;width:1rem;vertical-align:middle;accent-color:#0ea5e9}.configuration-value .checkbox label{display:flex;min-height:1.75rem;align-items:center;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-value .error-detail{margin-top:.125rem;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.configuration-value .help-block{margin-top:.125rem;display:block;font-size:.7rem;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.configuration-secret{min-width:0px}.configuration-item-actions,.configuration-array-actions{display:flex;flex-wrap:wrap;gap:.125rem}
|
||||
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{r as g,R as a}from"./index-dJH5nUWV.js";const d="64px",u={},k=({src:s,light:t,oEmbedUrl:o,onClickPreview:n,playIcon:b,previewTabIndex:_,previewAriaLabel:p})=>{const[f,r]=g.useState(null);g.useEffect(()=>{!s||!t||!o||x({src:s,light:t,oEmbedUrl:o})},[s,t,o]);const x=async({src:e,light:i,oEmbedUrl:E})=>{if(a.isValidElement(i))return;if(typeof i=="string"){r(i);return}if(u[e]){r(u[e]);return}r(null);const m=await(await fetch(E.replace("{url}",e))).json();if(m.thumbnail_url){const h=m.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");r(h),u[e]=h}},I=e=>{(e.key==="Enter"||e.key===" ")&&n?.(e)},v=e=>{n?.(e)},l=a.isValidElement(t),y={display:"flex",alignItems:"center",justifyContent:"center"},c={preview:{width:"100%",height:"100%",backgroundImage:f&&!l?`url(${f})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...y},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:d,width:d,height:d,position:l?"absolute":void 0,...y},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},w=a.createElement("div",{style:c.shadow,className:"react-player__shadow"},a.createElement("div",{style:c.playIcon,className:"react-player__play-icon"}));return a.createElement("div",{style:c.preview,className:"react-player__preview",tabIndex:_,onClick:v,onKeyDown:I,...p?{"aria-label":p}:{}},l?t:null,b||w)};var C=k;export{C as default};
|
||||
//# sourceMappingURL=Preview-BBzOMZjy.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{r as g,R as a}from"./index-5EWtcenj.js";const d="64px",u={},k=({src:s,light:t,oEmbedUrl:o,onClickPreview:n,playIcon:b,previewTabIndex:_,previewAriaLabel:p})=>{const[f,r]=g.useState(null);g.useEffect(()=>{!s||!t||!o||x({src:s,light:t,oEmbedUrl:o})},[s,t,o]);const x=async({src:e,light:i,oEmbedUrl:E})=>{if(a.isValidElement(i))return;if(typeof i=="string"){r(i);return}if(u[e]){r(u[e]);return}r(null);const m=await(await fetch(E.replace("{url}",e))).json();if(m.thumbnail_url){const h=m.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");r(h),u[e]=h}},I=e=>{(e.key==="Enter"||e.key===" ")&&n?.(e)},v=e=>{n?.(e)},l=a.isValidElement(t),y={display:"flex",alignItems:"center",justifyContent:"center"},c={preview:{width:"100%",height:"100%",backgroundImage:f&&!l?`url(${f})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...y},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:d,width:d,height:d,position:l?"absolute":void 0,...y},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},w=a.createElement("div",{style:c.shadow,className:"react-player__shadow"},a.createElement("div",{style:c.playIcon,className:"react-player__play-icon"}));return a.createElement("div",{style:c.preview,className:"react-player__preview",tabIndex:_,onClick:v,onKeyDown:I,...p?{"aria-label":p}:{}},l?t:null,b||w)};var C=k;export{C as default};
|
||||
//# sourceMappingURL=Preview-CKTJqKe-.js.map
|
||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-n0JxE1Mv.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
|
||||
//# sourceMappingURL=SetupApp-CaWv689i.js.map
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-5EWtcenj.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
|
||||
//# sourceMappingURL=SetupApp-DN7XBkOe.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-dJH5nUWV.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
|
||||
//# sourceMappingURL=SetupApp-Ya006OtX.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
function u(i){return class extends i{_playedRanges=[];_currentPlayedRange=null;_RANGE_EPSILON=.5;_seeking=!1;connectedCallback(){super.connectedCallback&&super.connectedCallback(),this.addEventListener("play",()=>this.onPlaybackStart({time:this.currentTime})),this.addEventListener("pause",()=>this.onPlaybackStop({time:this.currentTime})),this.addEventListener("ended",()=>this.onPlaybackStop({time:this.currentTime})),this.addEventListener("seeking",()=>this.onSeeking()),this.addEventListener("seeked",()=>this.onSeeked({time:this.currentTime}))}onPlaybackStart(e={}){this._seeking=!1;const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._currentPlayedRange||(this._currentPlayedRange={start:n,end:n})}onSeeking(){this._seeking=!0,this._commitCurrentRange()}onSeeked(e={}){this._seeking=!1;const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._currentPlayedRange={start:n,end:n}}onPlaybackStop(e={}){const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._commitCurrentRange(n)}_commitCurrentRange(e){if(!this._currentPlayedRange)return;typeof e=="number"&&(this._currentPlayedRange.end=e);const{start:t,end:n}=this._currentPlayedRange;this._currentPlayedRange=null,this.addPlayedRange(t,n)}addPlayedRange(e,t){if(e>=t)return;const n=this._RANGE_EPSILON,c=[...this._playedRanges,{start:e,end:t}];c.sort((a,s)=>a.start-s.start);const r=[];for(const a of c){if(!r.length){r.push({...a});continue}const s=r[r.length-1];a.start<=s.end+n?(s.start=Math.min(s.start,a.start),s.end=Math.max(s.end,a.end)):r.push({...a})}this._playedRanges=r}get played(){const e=this.currentTime;return!this.paused&&!this._currentPlayedRange&&typeof e=="number"&&(this._currentPlayedRange={start:e,end:e}),this._currentPlayedRange&&typeof e=="number"&&(e>this._currentPlayedRange.end&&(this._currentPlayedRange.end=e),this.addPlayedRange(this._currentPlayedRange.start,this._currentPlayedRange.end)),this._playedRanges.length?l(this._playedRanges.map(t=>[t.start,t.end])):l([[0,0]])}}}function l(i){return Object.defineProperties(i,{start:{value:d=>i[d][0]},end:{value:d=>i[d][1]}}),i}export{u as M};
|
||||
//# sourceMappingURL=index-B4nclofW.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
import{_ as N,R as A}from"./index-dJH5nUWV.js";import{M as D,C as S}from"./mixin-BY5xW4uI.js";class P extends D(S){static shadowRootOptions={...S.shadowRootOptions};static getTemplateHTML=i=>{const{src:n,...o}=i;return S.getTemplateHTML(o)};#t;attributeChangedCallback(i,n,o){i!=="src"&&super.attributeChangedCallback(i,n,o),i==="src"&&n!=o&&this.load()}async _initThumbnails(i){const n=async(d,m)=>{const v=[],g=i.timescale||1,h=i.startNumber||1,E=i.presentationTimeOffset?i.presentationTimeOffset/g:0,_=i.segmentDuration;for(let l=0;l<d;l++){const T=C({thIndex:l,thduration:m,ttiles:d,tduration:_,startNumber:h,pto:E}),w=T+m,M=new Promise((f,y)=>{this.api.provideThumbnail(T,({url:a,width:s,height:c,x:p,y:b})=>{try{const R=new VTTCue(T,w,`${a}#xywh=${p},${b},${s},${c}`);f(R)}catch(R){y(R)}})});v.push(M)}return await Promise.all(v).catch(l=>console.error("Error processing thumbnails",l))},{totalThumbnails:o,thumbnailDuration:e}=$(i),u=await n(o,e);let r=this.nativeEl.querySelector('track[label="thumbnails"]');if(!r){r=O(),this.nativeEl.appendChild(r);const d=x(u);r.src=d,r.dispatchEvent(new Event("change"))}}async load(){if(this.#t){this.api.attachSource(this.src);return}this.#t=!0;const i=await N(()=>import("./dash.all.min-C9gxBrL1.js"),[]);this.api=i.MediaPlayer().create(),this.api.initialize(this.nativeEl,this.src,this.autoplay),this.api.on(i.MediaPlayer.events.STREAM_INITIALIZED,()=>{const n=this.api.getRepresentationsByType("video");let o=this.videoTracks.getTrackById("main");o||(o=this.addVideoTrack("main"),o.id="main",o.selected=!0),n.forEach(e=>{const u=e.bandwidth??e.bitrate??(Number.isFinite(e.bitrateInKbit)?e.bitrateInKbit*1e3:void 0),r=o.addRendition(e.id,e.width,e.height,e.mimeType??e.codec,u);r.id=e.id}),this.videoRenditions.addEventListener("change",()=>{const e=this.videoRenditions[this.videoRenditions.selectedIndex];e?.id?(this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),this.api.setRepresentationForTypeById("video",e.id,!0)):this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}})}),this.api.isDynamic()||this.api.getRepresentationsByType("image").forEach(async(u,r)=>{r>0||this._initThumbnails(u)})})}}function $(t){var i,n;const o=t.essentialProperties[0],[e,u]=o.value.split("x").map(Number),r=e*u,d=((n=(i=t.adaptation)==null?void 0:i.period)==null?void 0:n.duration)||null,m=t.segmentDuration,v=t.timescale||1,h=m/v/r;return{totalThumbnails:d!=null?Math.ceil(d/h):Math.ceil(m/h),thumbnailDuration:h}}function C({thIndex:t,tduration:i,thduration:n,ttiles:o,startNumber:e,pto:u}){const r=Math.floor(t/o)+e,d=t%o+1,m=(r-1)*i-u,v=(d-1)*n;return m+v}function O(){const t=document.createElement("track");return t.kind="metadata",t.label="thumbnails",t.srclang="en",t.mode="hidden",t.default=!0,t}function x(t){let i=`WEBVTT
|
||||
|
||||
`;for(const e of t)i+=`${o(e.startTime)} --> ${o(e.endTime)}
|
||||
`,i+=`${e.text}
|
||||
|
||||
`;const n=new Blob([i],{type:"text/vtt"});return URL.createObjectURL(n);function o(e){const u=String(Math.floor(e/3600)).padStart(2,"0"),r=String(Math.floor(e%3600/60)).padStart(2,"0"),d=(e%60).toFixed(3).padStart(6,"0");return`${u}:${r}:${d}`}}globalThis.customElements&&!globalThis.customElements.get("dash-video")&&globalThis.customElements.define("dash-video",P);var B=P,H=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),F={className:"class",htmlFor:"for"};function I(t){return t.toLowerCase()}function k(t){if(typeof t=="boolean")return t?"":void 0;if(typeof t!="function"&&!(typeof t=="object"&&t!==null))return t}function W({react:t,tagName:i,elementClass:n,events:o,displayName:e,defaultProps:u,toAttributeName:r=I,toAttributeValue:d=k}){const m=Number.parseInt(t.version)>=19,v=t.forwardRef((g,h)=>{var E,_;const l=t.useRef(null),T=t.useRef(new Map),w={},M={},f={},y={};for(const[a,s]of Object.entries(g)){if(H.has(a)){f[a]=s;continue}const c=r(F[a]??a);if(n.prototype&&a in n.prototype&&!(a in(((E=globalThis.HTMLElement)==null?void 0:E.prototype)??{}))&&!((_=n.observedAttributes)!=null&&_.some(b=>b===c))){y[a]=s;continue}if(a.startsWith("on")){w[a]=s;continue}const p=d(s);if(c&&p!=null&&(M[c]=String(p),m||(f[c]=p)),c&&m){const b=k(s);p!==b?f[c]=p:f[c]=s}}if(typeof window<"u"){for(const a in w){const s=w[a],c=a.endsWith("Capture"),p=(o?.[a]??a.slice(2).toLowerCase()).slice(0,c?-7:void 0);t.useLayoutEffect(()=>{const b=l?.current;if(!(!b||typeof s!="function"))return b.addEventListener(p,s,c),()=>{b.removeEventListener(p,s,c)}},[l?.current,s])}t.useLayoutEffect(()=>{if(l.current===null)return;const a=new Map;for(const s in y)L(l.current,s,y[s]),T.current.delete(s),a.set(s,y[s]);for(const[s,c]of T.current)L(l.current,s,void 0);T.current=a})}if(typeof window>"u"&&n?.getTemplateHTML&&n?.shadowRootOptions){const{mode:a,delegatesFocus:s}=n.shadowRootOptions,c=t.createElement("template",{shadowrootmode:a,shadowrootdelegatesfocus:s,dangerouslySetInnerHTML:{__html:n.getTemplateHTML(M,g)},key:"ce-la-react-ssr-template-shadow-root"});f.children=[c,f.children]}return t.createElement(i,{...u,...f,ref:t.useCallback(a=>{l.current=a,typeof h=="function"?h(a):h!==null&&(h.current=a)},[h])},f.children)});return v.displayName=e??n.name,v}function L(t,i,n){var o;t[i]=n,n==null&&i in(((o=globalThis.HTMLElement)==null?void 0:o.prototype)??{})&&t.removeAttribute(i)}var V=W({react:A,tagName:"dash-video",elementClass:B,toAttributeName(t){return t==="muted"?"":t==="defaultMuted"?"muted":I(t)}});export{V as default};
|
||||
//# sourceMappingURL=react-C61Q8sy9.js.map
|
||||
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
@@ -0,0 +1,35 @@
|
||||
import{R as W}from"./index-dJH5nUWV.js";const $=["abort","canplay","canplaythrough","durationchange","emptied","encrypted","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting","waitingforkey","resize","enterpictureinpicture","leavepictureinpicture","webkitbeginfullscreen","webkitendfullscreen","webkitpresentationmodechanged"],A=globalThis.document?.createElement("template");A&&(A.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);const N=(i,{tag:t,is:s})=>{const d=globalThis.document?.createElement(t,{is:s}),f=d?S(d):[];return class g extends i{static Events=$;static template=A;static skipAttributes=[];static#t;static get observedAttributes(){return g.#l(),[...d?.constructor?.observedAttributes??[],"autopictureinpicture","disablepictureinpicture","disableremoteplayback","autoplay","controls","controlslist","crossorigin","loop","muted","playsinline","poster","preload","src"]}static#l(){if(this.#t)return;this.#t=!0;const e=new Set(this.observedAttributes);e.delete("muted");for(let o of f){if(o in this.prototype)continue;if(typeof d[o]=="function")this.prototype[o]=function(...r){this.#e();const a=()=>this.call?this.call(o,...r):this.nativeEl[o].apply(this.nativeEl,r);return this.loadComplete&&!this.isLoaded?this.loadComplete.then(a):a()};else{let r={get(){this.#e();let a=o.toLowerCase();if(e.has(a)){const p=this.getAttribute(a);return p===null?!1:p===""?!0:p}return this.get?.(o)??this.nativeEl?.[o]??this.#s[o]}};o!==o.toUpperCase()&&(r.set=async function(a){this.#e();let p=o.toLowerCase();if(e.has(p)){a===!0||a===!1||a==null?this.toggleAttribute(p,!!a):this.setAttribute(p,a);return}if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.set){this.set(o,a);return}this.nativeEl[o]=a}),Object.defineProperty(this.prototype,o,r)}}}#o;#n;#r=!1;#i=!1;#a;#s;constructor(){super(),this.shadowRoot||(this.attachShadow({mode:"open"}),this.shadowRoot.append(this.constructor.template.content.cloneNode(!0))),this.load!==g.prototype.load&&(this.loadComplete=new O)}get loadComplete(){return this.#n}set loadComplete(e){this.#i=!1,this.#n=e,e?.then(()=>{this.#i=!0})}get isLoaded(){return this.#i}get nativeEl(){return this.#a??this.shadowRoot.querySelector(t)??this.querySelector(t)}set nativeEl(e){this.#a=e}get defaultMuted(){return this.hasAttribute("muted")}set defaultMuted(e){this.toggleAttribute("muted",!!e)}get src(){return this.getAttribute("src")}set src(e){this.setAttribute("src",`${e}`)}get preload(){return this.getAttribute("preload")??this.nativeEl?.preload}set preload(e){this.setAttribute("preload",`${e}`)}async#e(){if(this.#o)return;this.#o=!0,this.#u(),this.#c();for(let u of f)this.#d(u);const e=new Map,o=this.shadowRoot.querySelector("slot:not([name])");o?.addEventListener("slotchange",()=>{const u=new Map(e);o.assignedElements().filter(r=>["track","source"].includes(r.localName)).forEach(async r=>{u.delete(r);let a=e.get(r);a||(a=r.cloneNode(),e.set(r,a)),this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.nativeEl.append?.(a)}),u.forEach(r=>r.remove())});for(let u of this.constructor.Events)this.shadowRoot.addEventListener?.(u,r=>{r.target===this.nativeEl&&this.dispatchEvent(new CustomEvent(r.type,{detail:r.detail}))},!0)}#d(e){if(Object.prototype.hasOwnProperty.call(this,e)){const o=this[e];delete this[e],this[e]=o}}#u(){const e=document.createElement(t,{is:s});e.muted=this.hasAttribute("muted");for(let{name:o,value:u}of this.attributes)e.setAttribute(o,u);this.#s={};for(let o of S(e))this.#s[o]=e[o];e.removeAttribute("src"),e.load()}async#c(){if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,!this.nativeEl){const e=document.createElement(t,{is:s});e.part=t,this.shadowRoot.append(e)}this.nativeEl.muted=this.hasAttribute("muted")}attributeChangedCallback(e,o,u){this.#e(),e==="src"&&this.load!==g.prototype.load&&this.#h(),this.#p(e,o,u)}async#h(){this.#r&&(this.loadComplete=new O),this.#r=!0,await Promise.resolve(),await this.load(),this.loadComplete?.resolve(),await this.loadComplete}async#p(e,o,u){this.loadComplete&&!this.isLoaded&&await this.loadComplete,!["id","class",...this.constructor.skipAttributes].includes(e)&&(u===null?this.nativeEl.removeAttribute?.(e):this.nativeEl.setAttribute?.(e,u))}connectedCallback(){this.#e()}}};function S(i){let t=[];for(let s=Object.getPrototypeOf(i);s&&s!==HTMLElement.prototype;s=Object.getPrototypeOf(s))t.push(...Object.getOwnPropertyNames(s));return t}class O extends Promise{constructor(t=()=>{}){let s,d;super((f,c)=>{t(f,c),s=f,d=c}),this.resolve=s,this.reject=d}}const B=globalThis.document?N(HTMLElement,{tag:"video"}):class{};globalThis.document&&N(HTMLElement,{tag:"audio"});var H,R;const L=(H=globalThis.document)==null?void 0:H.createElement("template");L&&(L.innerHTML=`
|
||||
<div class="wistia_embed"></div>
|
||||
`);const P=(R=globalThis.document)==null?void 0:R.createElement("template");P&&(P.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
::slotted(.wistia_embed) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);function D(i,t){if(Object.hasOwn(i,t)){const s=i[t];delete i[t],i[t]=s}}class q extends B{static template=P;static skipAttributes=["src"];#t=null;constructor(){super(),D(this,"config")}get config(){return this.#t}set config(t){this.#t=t}get nativeEl(){var t;return((t=this.api)==null?void 0:t.elem())??this.querySelector("video")}async load(){var t;if((t=this.querySelector(".wistia_embed"))==null||t.remove(),!this.src)return;await new Promise(o=>setTimeout(o,50));const s=/(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/i,d=this.src.match(s)[1],f={autoPlay:this.autoplay,silentAutoPlay:this.autoplay?"allow":void 0,preload:this.preload??"metadata",playsinline:this.playsInline,endVideoBehavior:this.loop?"loop":void 0,chromeless:!this.controls,playButton:this.controls,muted:this.defaultMuted,...this.#t??{}};this.append(L.content.cloneNode(!0));const c=this.querySelector(".wistia_embed");c.id||(c.id=z(d)),c.classList.add(`wistia_async_${d}`),await F("https://fast.wistia.com/assets/external/E-v1.js","Wistia"),this.api=await new Promise(o=>{globalThis._wq.push({id:c.id,onReady:o,options:f})})}async attributeChangedCallback(t,s,d){if(t==="controls"){switch(await this.loadComplete,t){case"controls":this.api.bigPlayButtonEnabled(this.controls),this.controls?this.api.releaseChromeless():this.api.requestChromeless();break}return}super.attributeChangedCallback(t,s,d)}get duration(){var t;return(t=this.api)==null?void 0:t.duration()}async play(){return await this.loadComplete,this.api.play(),new Promise(t=>this.addEventListener("playing",t))}}const T={};async function F(i,t){if(T[i])return T[i];if(self[t])return self[t];const s=new Promise((d,f)=>{const c=document.createElement("script");c.defer=!0,c.src=i,c.onload=()=>d(self[t]),c.onerror=f,document.head.append(c)});return T[i]=s,s}let U=0;function z(i){const t=++U;return`${i}${t}`}globalThis.customElements&&!globalThis.customElements.get("wistia-video")&&globalThis.customElements.define("wistia-video",q);var V=q,G=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),J={className:"class",htmlFor:"for"};function I(i){return i.toLowerCase()}function j(i){if(typeof i=="boolean")return i?"":void 0;if(typeof i!="function"&&!(typeof i=="object"&&i!==null))return i}function K({react:i,tagName:t,elementClass:s,events:d,displayName:f,defaultProps:c,toAttributeName:e=I,toAttributeValue:o=j}){const u=Number.parseInt(i.version)>=19,r=i.forwardRef((a,p)=>{var M,k;const m=i.useRef(null),E=i.useRef(new Map),C={},_={},v={},w={};for(const[n,l]of Object.entries(a)){if(G.has(n)){v[n]=l;continue}const h=e(J[n]??n);if(s.prototype&&n in s.prototype&&!(n in(((M=globalThis.HTMLElement)==null?void 0:M.prototype)??{}))&&!((k=s.observedAttributes)!=null&&k.some(b=>b===h))){w[n]=l;continue}if(n.startsWith("on")){C[n]=l;continue}const y=o(l);if(h&&y!=null&&(_[h]=String(y),u||(v[h]=y)),h&&u){const b=j(l);y!==b?v[h]=y:v[h]=l}}if(typeof window<"u"){for(const n in C){const l=C[n],h=n.endsWith("Capture"),y=(d?.[n]??n.slice(2).toLowerCase()).slice(0,h?-7:void 0);i.useLayoutEffect(()=>{const b=m?.current;if(!(!b||typeof l!="function"))return b.addEventListener(y,l,h),()=>{b.removeEventListener(y,l,h)}},[m?.current,l])}i.useLayoutEffect(()=>{if(m.current===null)return;const n=new Map;for(const l in w)x(m.current,l,w[l]),E.current.delete(l),n.set(l,w[l]);for(const[l,h]of E.current)x(m.current,l,void 0);E.current=n})}if(typeof window>"u"&&s?.getTemplateHTML&&s?.shadowRootOptions){const{mode:n,delegatesFocus:l}=s.shadowRootOptions,h=i.createElement("template",{shadowrootmode:n,shadowrootdelegatesfocus:l,dangerouslySetInnerHTML:{__html:s.getTemplateHTML(_,a)},key:"ce-la-react-ssr-template-shadow-root"});v.children=[h,v.children]}return i.createElement(t,{...c,...v,ref:i.useCallback(n=>{m.current=n,typeof p=="function"?p(n):p!==null&&(p.current=n)},[p])},v.children)});return r.displayName=f??s.name,r}function x(i,t,s){var d;i[t]=s,s==null&&t in(((d=globalThis.HTMLElement)==null?void 0:d.prototype)??{})&&i.removeAttribute(t)}var X=K({react:W,tagName:"wistia-video",elementClass:V,toAttributeName(i){return i==="muted"?"":i==="defaultMuted"?"muted":I(i)}});export{X as default};
|
||||
//# sourceMappingURL=react-CVBBC_mw.js.map
|
||||
File diff suppressed because one or more lines are too long
+23
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+23
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
@@ -0,0 +1,35 @@
|
||||
import{R as W}from"./index-5EWtcenj.js";const $=["abort","canplay","canplaythrough","durationchange","emptied","encrypted","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting","waitingforkey","resize","enterpictureinpicture","leavepictureinpicture","webkitbeginfullscreen","webkitendfullscreen","webkitpresentationmodechanged"],A=globalThis.document?.createElement("template");A&&(A.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);const N=(i,{tag:t,is:s})=>{const d=globalThis.document?.createElement(t,{is:s}),f=d?S(d):[];return class g extends i{static Events=$;static template=A;static skipAttributes=[];static#t;static get observedAttributes(){return g.#l(),[...d?.constructor?.observedAttributes??[],"autopictureinpicture","disablepictureinpicture","disableremoteplayback","autoplay","controls","controlslist","crossorigin","loop","muted","playsinline","poster","preload","src"]}static#l(){if(this.#t)return;this.#t=!0;const e=new Set(this.observedAttributes);e.delete("muted");for(let o of f){if(o in this.prototype)continue;if(typeof d[o]=="function")this.prototype[o]=function(...r){this.#e();const a=()=>this.call?this.call(o,...r):this.nativeEl[o].apply(this.nativeEl,r);return this.loadComplete&&!this.isLoaded?this.loadComplete.then(a):a()};else{let r={get(){this.#e();let a=o.toLowerCase();if(e.has(a)){const p=this.getAttribute(a);return p===null?!1:p===""?!0:p}return this.get?.(o)??this.nativeEl?.[o]??this.#s[o]}};o!==o.toUpperCase()&&(r.set=async function(a){this.#e();let p=o.toLowerCase();if(e.has(p)){a===!0||a===!1||a==null?this.toggleAttribute(p,!!a):this.setAttribute(p,a);return}if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.set){this.set(o,a);return}this.nativeEl[o]=a}),Object.defineProperty(this.prototype,o,r)}}}#o;#n;#r=!1;#i=!1;#a;#s;constructor(){super(),this.shadowRoot||(this.attachShadow({mode:"open"}),this.shadowRoot.append(this.constructor.template.content.cloneNode(!0))),this.load!==g.prototype.load&&(this.loadComplete=new O)}get loadComplete(){return this.#n}set loadComplete(e){this.#i=!1,this.#n=e,e?.then(()=>{this.#i=!0})}get isLoaded(){return this.#i}get nativeEl(){return this.#a??this.shadowRoot.querySelector(t)??this.querySelector(t)}set nativeEl(e){this.#a=e}get defaultMuted(){return this.hasAttribute("muted")}set defaultMuted(e){this.toggleAttribute("muted",!!e)}get src(){return this.getAttribute("src")}set src(e){this.setAttribute("src",`${e}`)}get preload(){return this.getAttribute("preload")??this.nativeEl?.preload}set preload(e){this.setAttribute("preload",`${e}`)}async#e(){if(this.#o)return;this.#o=!0,this.#u(),this.#c();for(let u of f)this.#d(u);const e=new Map,o=this.shadowRoot.querySelector("slot:not([name])");o?.addEventListener("slotchange",()=>{const u=new Map(e);o.assignedElements().filter(r=>["track","source"].includes(r.localName)).forEach(async r=>{u.delete(r);let a=e.get(r);a||(a=r.cloneNode(),e.set(r,a)),this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.nativeEl.append?.(a)}),u.forEach(r=>r.remove())});for(let u of this.constructor.Events)this.shadowRoot.addEventListener?.(u,r=>{r.target===this.nativeEl&&this.dispatchEvent(new CustomEvent(r.type,{detail:r.detail}))},!0)}#d(e){if(Object.prototype.hasOwnProperty.call(this,e)){const o=this[e];delete this[e],this[e]=o}}#u(){const e=document.createElement(t,{is:s});e.muted=this.hasAttribute("muted");for(let{name:o,value:u}of this.attributes)e.setAttribute(o,u);this.#s={};for(let o of S(e))this.#s[o]=e[o];e.removeAttribute("src"),e.load()}async#c(){if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,!this.nativeEl){const e=document.createElement(t,{is:s});e.part=t,this.shadowRoot.append(e)}this.nativeEl.muted=this.hasAttribute("muted")}attributeChangedCallback(e,o,u){this.#e(),e==="src"&&this.load!==g.prototype.load&&this.#h(),this.#p(e,o,u)}async#h(){this.#r&&(this.loadComplete=new O),this.#r=!0,await Promise.resolve(),await this.load(),this.loadComplete?.resolve(),await this.loadComplete}async#p(e,o,u){this.loadComplete&&!this.isLoaded&&await this.loadComplete,!["id","class",...this.constructor.skipAttributes].includes(e)&&(u===null?this.nativeEl.removeAttribute?.(e):this.nativeEl.setAttribute?.(e,u))}connectedCallback(){this.#e()}}};function S(i){let t=[];for(let s=Object.getPrototypeOf(i);s&&s!==HTMLElement.prototype;s=Object.getPrototypeOf(s))t.push(...Object.getOwnPropertyNames(s));return t}class O extends Promise{constructor(t=()=>{}){let s,d;super((f,c)=>{t(f,c),s=f,d=c}),this.resolve=s,this.reject=d}}const B=globalThis.document?N(HTMLElement,{tag:"video"}):class{};globalThis.document&&N(HTMLElement,{tag:"audio"});var H,R;const L=(H=globalThis.document)==null?void 0:H.createElement("template");L&&(L.innerHTML=`
|
||||
<div class="wistia_embed"></div>
|
||||
`);const P=(R=globalThis.document)==null?void 0:R.createElement("template");P&&(P.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
::slotted(.wistia_embed) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);function D(i,t){if(Object.hasOwn(i,t)){const s=i[t];delete i[t],i[t]=s}}class q extends B{static template=P;static skipAttributes=["src"];#t=null;constructor(){super(),D(this,"config")}get config(){return this.#t}set config(t){this.#t=t}get nativeEl(){var t;return((t=this.api)==null?void 0:t.elem())??this.querySelector("video")}async load(){var t;if((t=this.querySelector(".wistia_embed"))==null||t.remove(),!this.src)return;await new Promise(o=>setTimeout(o,50));const s=/(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/i,d=this.src.match(s)[1],f={autoPlay:this.autoplay,silentAutoPlay:this.autoplay?"allow":void 0,preload:this.preload??"metadata",playsinline:this.playsInline,endVideoBehavior:this.loop?"loop":void 0,chromeless:!this.controls,playButton:this.controls,muted:this.defaultMuted,...this.#t??{}};this.append(L.content.cloneNode(!0));const c=this.querySelector(".wistia_embed");c.id||(c.id=z(d)),c.classList.add(`wistia_async_${d}`),await F("https://fast.wistia.com/assets/external/E-v1.js","Wistia"),this.api=await new Promise(o=>{globalThis._wq.push({id:c.id,onReady:o,options:f})})}async attributeChangedCallback(t,s,d){if(t==="controls"){switch(await this.loadComplete,t){case"controls":this.api.bigPlayButtonEnabled(this.controls),this.controls?this.api.releaseChromeless():this.api.requestChromeless();break}return}super.attributeChangedCallback(t,s,d)}get duration(){var t;return(t=this.api)==null?void 0:t.duration()}async play(){return await this.loadComplete,this.api.play(),new Promise(t=>this.addEventListener("playing",t))}}const T={};async function F(i,t){if(T[i])return T[i];if(self[t])return self[t];const s=new Promise((d,f)=>{const c=document.createElement("script");c.defer=!0,c.src=i,c.onload=()=>d(self[t]),c.onerror=f,document.head.append(c)});return T[i]=s,s}let U=0;function z(i){const t=++U;return`${i}${t}`}globalThis.customElements&&!globalThis.customElements.get("wistia-video")&&globalThis.customElements.define("wistia-video",q);var V=q,G=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),J={className:"class",htmlFor:"for"};function I(i){return i.toLowerCase()}function j(i){if(typeof i=="boolean")return i?"":void 0;if(typeof i!="function"&&!(typeof i=="object"&&i!==null))return i}function K({react:i,tagName:t,elementClass:s,events:d,displayName:f,defaultProps:c,toAttributeName:e=I,toAttributeValue:o=j}){const u=Number.parseInt(i.version)>=19,r=i.forwardRef((a,p)=>{var M,k;const m=i.useRef(null),E=i.useRef(new Map),C={},_={},v={},w={};for(const[n,l]of Object.entries(a)){if(G.has(n)){v[n]=l;continue}const h=e(J[n]??n);if(s.prototype&&n in s.prototype&&!(n in(((M=globalThis.HTMLElement)==null?void 0:M.prototype)??{}))&&!((k=s.observedAttributes)!=null&&k.some(b=>b===h))){w[n]=l;continue}if(n.startsWith("on")){C[n]=l;continue}const y=o(l);if(h&&y!=null&&(_[h]=String(y),u||(v[h]=y)),h&&u){const b=j(l);y!==b?v[h]=y:v[h]=l}}if(typeof window<"u"){for(const n in C){const l=C[n],h=n.endsWith("Capture"),y=(d?.[n]??n.slice(2).toLowerCase()).slice(0,h?-7:void 0);i.useLayoutEffect(()=>{const b=m?.current;if(!(!b||typeof l!="function"))return b.addEventListener(y,l,h),()=>{b.removeEventListener(y,l,h)}},[m?.current,l])}i.useLayoutEffect(()=>{if(m.current===null)return;const n=new Map;for(const l in w)x(m.current,l,w[l]),E.current.delete(l),n.set(l,w[l]);for(const[l,h]of E.current)x(m.current,l,void 0);E.current=n})}if(typeof window>"u"&&s?.getTemplateHTML&&s?.shadowRootOptions){const{mode:n,delegatesFocus:l}=s.shadowRootOptions,h=i.createElement("template",{shadowrootmode:n,shadowrootdelegatesfocus:l,dangerouslySetInnerHTML:{__html:s.getTemplateHTML(_,a)},key:"ce-la-react-ssr-template-shadow-root"});v.children=[h,v.children]}return i.createElement(t,{...c,...v,ref:i.useCallback(n=>{m.current=n,typeof p=="function"?p(n):p!==null&&(p.current=n)},[p])},v.children)});return r.displayName=f??s.name,r}function x(i,t,s){var d;i[t]=s,s==null&&t in(((d=globalThis.HTMLElement)==null?void 0:d.prototype)??{})&&i.removeAttribute(t)}var X=K({react:W,tagName:"wistia-video",elementClass:V,toAttributeName(i){return i==="muted"?"":i==="defaultMuted"?"muted":I(i)}});export{X as default};
|
||||
//# sourceMappingURL=react-DxdYv-AY.js.map
|
||||
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
import{_ as N,R as A}from"./index-5EWtcenj.js";import{M as D,C as S}from"./mixin-BY5xW4uI.js";class P extends D(S){static shadowRootOptions={...S.shadowRootOptions};static getTemplateHTML=i=>{const{src:n,...o}=i;return S.getTemplateHTML(o)};#t;attributeChangedCallback(i,n,o){i!=="src"&&super.attributeChangedCallback(i,n,o),i==="src"&&n!=o&&this.load()}async _initThumbnails(i){const n=async(d,m)=>{const v=[],g=i.timescale||1,h=i.startNumber||1,E=i.presentationTimeOffset?i.presentationTimeOffset/g:0,_=i.segmentDuration;for(let l=0;l<d;l++){const T=C({thIndex:l,thduration:m,ttiles:d,tduration:_,startNumber:h,pto:E}),w=T+m,M=new Promise((f,y)=>{this.api.provideThumbnail(T,({url:a,width:s,height:c,x:p,y:b})=>{try{const R=new VTTCue(T,w,`${a}#xywh=${p},${b},${s},${c}`);f(R)}catch(R){y(R)}})});v.push(M)}return await Promise.all(v).catch(l=>console.error("Error processing thumbnails",l))},{totalThumbnails:o,thumbnailDuration:e}=$(i),u=await n(o,e);let r=this.nativeEl.querySelector('track[label="thumbnails"]');if(!r){r=O(),this.nativeEl.appendChild(r);const d=x(u);r.src=d,r.dispatchEvent(new Event("change"))}}async load(){if(this.#t){this.api.attachSource(this.src);return}this.#t=!0;const i=await N(()=>import("./dash.all.min-C9gxBrL1.js"),[]);this.api=i.MediaPlayer().create(),this.api.initialize(this.nativeEl,this.src,this.autoplay),this.api.on(i.MediaPlayer.events.STREAM_INITIALIZED,()=>{const n=this.api.getRepresentationsByType("video");let o=this.videoTracks.getTrackById("main");o||(o=this.addVideoTrack("main"),o.id="main",o.selected=!0),n.forEach(e=>{const u=e.bandwidth??e.bitrate??(Number.isFinite(e.bitrateInKbit)?e.bitrateInKbit*1e3:void 0),r=o.addRendition(e.id,e.width,e.height,e.mimeType??e.codec,u);r.id=e.id}),this.videoRenditions.addEventListener("change",()=>{const e=this.videoRenditions[this.videoRenditions.selectedIndex];e?.id?(this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),this.api.setRepresentationForTypeById("video",e.id,!0)):this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}})}),this.api.isDynamic()||this.api.getRepresentationsByType("image").forEach(async(u,r)=>{r>0||this._initThumbnails(u)})})}}function $(t){var i,n;const o=t.essentialProperties[0],[e,u]=o.value.split("x").map(Number),r=e*u,d=((n=(i=t.adaptation)==null?void 0:i.period)==null?void 0:n.duration)||null,m=t.segmentDuration,v=t.timescale||1,h=m/v/r;return{totalThumbnails:d!=null?Math.ceil(d/h):Math.ceil(m/h),thumbnailDuration:h}}function C({thIndex:t,tduration:i,thduration:n,ttiles:o,startNumber:e,pto:u}){const r=Math.floor(t/o)+e,d=t%o+1,m=(r-1)*i-u,v=(d-1)*n;return m+v}function O(){const t=document.createElement("track");return t.kind="metadata",t.label="thumbnails",t.srclang="en",t.mode="hidden",t.default=!0,t}function x(t){let i=`WEBVTT
|
||||
|
||||
`;for(const e of t)i+=`${o(e.startTime)} --> ${o(e.endTime)}
|
||||
`,i+=`${e.text}
|
||||
|
||||
`;const n=new Blob([i],{type:"text/vtt"});return URL.createObjectURL(n);function o(e){const u=String(Math.floor(e/3600)).padStart(2,"0"),r=String(Math.floor(e%3600/60)).padStart(2,"0"),d=(e%60).toFixed(3).padStart(6,"0");return`${u}:${r}:${d}`}}globalThis.customElements&&!globalThis.customElements.get("dash-video")&&globalThis.customElements.define("dash-video",P);var B=P,H=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),F={className:"class",htmlFor:"for"};function I(t){return t.toLowerCase()}function k(t){if(typeof t=="boolean")return t?"":void 0;if(typeof t!="function"&&!(typeof t=="object"&&t!==null))return t}function W({react:t,tagName:i,elementClass:n,events:o,displayName:e,defaultProps:u,toAttributeName:r=I,toAttributeValue:d=k}){const m=Number.parseInt(t.version)>=19,v=t.forwardRef((g,h)=>{var E,_;const l=t.useRef(null),T=t.useRef(new Map),w={},M={},f={},y={};for(const[a,s]of Object.entries(g)){if(H.has(a)){f[a]=s;continue}const c=r(F[a]??a);if(n.prototype&&a in n.prototype&&!(a in(((E=globalThis.HTMLElement)==null?void 0:E.prototype)??{}))&&!((_=n.observedAttributes)!=null&&_.some(b=>b===c))){y[a]=s;continue}if(a.startsWith("on")){w[a]=s;continue}const p=d(s);if(c&&p!=null&&(M[c]=String(p),m||(f[c]=p)),c&&m){const b=k(s);p!==b?f[c]=p:f[c]=s}}if(typeof window<"u"){for(const a in w){const s=w[a],c=a.endsWith("Capture"),p=(o?.[a]??a.slice(2).toLowerCase()).slice(0,c?-7:void 0);t.useLayoutEffect(()=>{const b=l?.current;if(!(!b||typeof s!="function"))return b.addEventListener(p,s,c),()=>{b.removeEventListener(p,s,c)}},[l?.current,s])}t.useLayoutEffect(()=>{if(l.current===null)return;const a=new Map;for(const s in y)L(l.current,s,y[s]),T.current.delete(s),a.set(s,y[s]);for(const[s,c]of T.current)L(l.current,s,void 0);T.current=a})}if(typeof window>"u"&&n?.getTemplateHTML&&n?.shadowRootOptions){const{mode:a,delegatesFocus:s}=n.shadowRootOptions,c=t.createElement("template",{shadowrootmode:a,shadowrootdelegatesfocus:s,dangerouslySetInnerHTML:{__html:n.getTemplateHTML(M,g)},key:"ce-la-react-ssr-template-shadow-root"});f.children=[c,f.children]}return t.createElement(i,{...u,...f,ref:t.useCallback(a=>{l.current=a,typeof h=="function"?h(a):h!==null&&(h.current=a)},[h])},f.children)});return v.displayName=e??n.name,v}function L(t,i,n){var o;t[i]=n,n==null&&i in(((o=globalThis.HTMLElement)==null?void 0:o.prototype)??{})&&t.removeAttribute(i)}var V=W({react:A,tagName:"dash-video",elementClass:B,toAttributeName(t){return t==="muted"?"":t==="defaultMuted"?"muted":I(t)}});export{V as default};
|
||||
//# sourceMappingURL=react-gdx3v4jU.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -12,8 +12,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-n0JxE1Mv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-xCRVLGjq.css">
|
||||
<script type="module" crossorigin src="/assets/index-dJH5nUWV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DOmNOCX4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -173,6 +173,8 @@ test('service definitions generate public feature paths without a separate regis
|
||||
{ key: 'homeAssistant', path: ['homeAssistant', 'enabled'] },
|
||||
{ key: 'neato', path: ['homeAssistant', 'neato', 'enabled'] },
|
||||
{ key: 'lift', path: ['homeAssistant', 'lift', 'enabled'] },
|
||||
// Activities remain independently configurable from the shared connection.
|
||||
{ key: 'homeAssistantActivities', path: ['homeAssistantActivities', 'enabled'] },
|
||||
{ key: 'roomCameras', path: ['roomCameras', 'enabled'] },
|
||||
{ key: 'ptzCamera', path: ['ptzCamera', 'enabled'] },
|
||||
{ key: 'kinect', path: ['kinect', 'enabled'] },
|
||||
@@ -182,6 +184,7 @@ test('service definitions generate public feature paths without a separate regis
|
||||
{ key: 'discord', path: ['discord', 'enabled'] },
|
||||
{ key: 'socials', path: ['socials', 'enabled'] },
|
||||
{ key: 'fleetReports', path: ['fleetReports', 'enabled'] },
|
||||
{ key: 'roverLearning', path: ['roverLearning', 'enabled'] },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ const bandwidthSavings = require('../helpers/bandwidthSavings.configuration');
|
||||
const audioForward = require('../services/audioForwardService/configuration');
|
||||
const audioLevels = require('../services/audioLevelsService/configuration');
|
||||
const homeAssistant = require('../services/homeAssistantService/configuration');
|
||||
const homeAssistantActivities = require('../services/homeAssistantActivitiesService/configuration');
|
||||
const roomAudio = require('../services/roomAudioService/configuration');
|
||||
const roomCameras = require('../services/roomCameraService/configuration');
|
||||
const ptzCamera = require('../services/ptzCameraService/configuration');
|
||||
const kinect = require('../services/kinectService/configuration');
|
||||
@@ -21,6 +23,7 @@ const barcodeScanner = require('../services/barcodeScannerService/configuration'
|
||||
const commands = require('../services/operatorCommandService/configuration');
|
||||
const discord = require('../services/discordBotService/configuration');
|
||||
const fleetReports = require('../services/fleetReportService/configuration');
|
||||
const roverLearning = require('../services/roverLearningService/configuration');
|
||||
|
||||
/*
|
||||
Object property order is preserved by JSON serialization and JSON Schema
|
||||
@@ -41,7 +44,10 @@ const definitions = [
|
||||
audioForward,
|
||||
audioLevels,
|
||||
homeAssistant,
|
||||
// Activities own a separate catalog even though the HA connection is shared.
|
||||
homeAssistantActivities,
|
||||
roomCameras,
|
||||
roomAudio,
|
||||
ptzCamera,
|
||||
kinect,
|
||||
balanceBoard,
|
||||
@@ -52,6 +58,7 @@ const definitions = [
|
||||
sessionConfiguration.socials,
|
||||
sessionConfiguration.driverAd,
|
||||
fleetReports,
|
||||
roverLearning,
|
||||
];
|
||||
|
||||
const defaultConfig = Object.fromEntries(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Configuration Database Migrations
|
||||
// Purpose: Applies ordered, transactional schema changes to the configuration and administration database.
|
||||
// Scope: Owns database structure only; configuration-document evolution belongs to the ordered definition and validation.
|
||||
// Scope: Upgrades database structure and stored configuration documents before current-schema validation.
|
||||
const migrations = [
|
||||
{
|
||||
version: 1,
|
||||
@@ -72,6 +72,46 @@ const migrations = [
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 3,
|
||||
run(db) {
|
||||
// Migrate historical revisions too, so rolling back settings cannot
|
||||
// restore the removed training-visit limit and break startup again.
|
||||
db.prepare(`
|
||||
UPDATE configuration_revisions
|
||||
SET config_json = json_remove(config_json, '$.roverLearning.training.roundsPerSession')
|
||||
WHERE json_type(config_json, '$.roverLearning.training.roundsPerSession') IS NOT NULL
|
||||
`).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 4,
|
||||
run(db) {
|
||||
const rows = db.prepare('SELECT id, config_json FROM configuration_revisions').all();
|
||||
const update = db.prepare('UPDATE configuration_revisions SET config_json = ? WHERE id = ?');
|
||||
// Upgrade history as well as the active revision so a settings rollback
|
||||
// cannot reintroduce fields rejected by the new Automations schema.
|
||||
for (const row of rows) {
|
||||
const config = JSON.parse(row.config_json);
|
||||
let changed = false;
|
||||
for (const item of config.homeAssistantActivities?.items || []) {
|
||||
const hasAction = Object.hasOwn(item, 'idleAction');
|
||||
const hasValue = Object.hasOwn(item, 'idleValue');
|
||||
if (!hasAction && !hasValue) continue;
|
||||
item.automations ??= {};
|
||||
item.automations.idle ??= {};
|
||||
// Preserve any explicitly saved new-format setting when both formats
|
||||
// exist, including an empty text value used to clear an entity.
|
||||
if (hasAction && !Object.hasOwn(item.automations.idle, 'action')) item.automations.idle.action = item.idleAction;
|
||||
if (hasValue && !Object.hasOwn(item.automations.idle, 'value')) item.automations.idle.value = item.idleValue;
|
||||
delete item.idleAction;
|
||||
delete item.idleValue;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) update.run(JSON.stringify(config), row.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function applySchemaMigrations(db) {
|
||||
|
||||
@@ -44,7 +44,7 @@ module.exports = {
|
||||
goal: 400,
|
||||
async run(ctx) {
|
||||
startStrobe(ctx, { endsAt: Date.now() + STROBE_MS, on: false });
|
||||
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room controls strobing for 60 seconds.' });
|
||||
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room lights strobing for 60 seconds.' });
|
||||
},
|
||||
async recover(ctx, effect) {
|
||||
if (!effect || Number(effect.endsAt || 0) <= Date.now()) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const logger = require('../../globals/logger').child('assignment');
|
||||
const { MODES, getMode, modeEvents } = require('../modeManager');
|
||||
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('../roleService');
|
||||
const roverManager = require('../roverManager');
|
||||
const { getOperatingMode } = require('../operatingModeService');
|
||||
const { compareRoversForAssignment } = require('./roverRanking');
|
||||
|
||||
const socketRefs = new Map(); // socketId -> socket
|
||||
@@ -131,7 +132,7 @@ roverManager.managerEvents.on('switch', ({ socketId, roverId }) => {
|
||||
});
|
||||
|
||||
function assignSocket(socket, options = {}) {
|
||||
if (!socket || isAdmin(socket) || getRole(socket) !== 'user') {
|
||||
if (!socket || getOperatingMode(socket) !== 'rover' || isAdmin(socket) || getRole(socket) !== 'user') {
|
||||
return;
|
||||
}
|
||||
// avoid double assignment
|
||||
@@ -311,13 +312,22 @@ function describeAssignment(socketId) {
|
||||
|
||||
module.exports = {
|
||||
assignmentEvents,
|
||||
resumeAssignment: assignSocket,
|
||||
releaseForOperatingMode: (socket) => {
|
||||
// Include direct admin control as well as automatic assignment ownership.
|
||||
const roverIds = new Set(roverManager.getRoversForSocket(socket.id));
|
||||
const assigned = assignments.get(socket.id);
|
||||
if (assigned) roverIds.add(assigned);
|
||||
waiting.delete(socket.id);
|
||||
roverIds.forEach((roverId) => forceRelease(roverId, socket.id));
|
||||
},
|
||||
describeAssignment,
|
||||
forceRelease,
|
||||
forceReleaseWithNotice,
|
||||
rerollAssignments,
|
||||
getAssignedRover: (socketId) => assignments.get(socketId) || null,
|
||||
moveAssignment: (socket, roverId, { releasePrevious = true } = {}) => {
|
||||
if (!socket || !roverId) return;
|
||||
if (!socket || !roverId || getOperatingMode(socket) !== 'rover') return;
|
||||
const previous = assignments.get(socket.id);
|
||||
if (previous && previous !== roverId && releasePrevious) {
|
||||
roverManager.releaseControl(previous, socket);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const path = require('path');
|
||||
const { subscribe } = require('../eventBus');
|
||||
|
||||
const DONE_CHARGING_SOUND_PATH = path.resolve(__dirname, '..', '..', '..', 'public', 'donecharging.wav');
|
||||
const DONE_CHARGING_SOUND_PATH = path.resolve(__dirname, '..', '..', '..', 'assets', 'donecharging.wav');
|
||||
|
||||
function registerChargeCompleteSound(deps) {
|
||||
const {
|
||||
@@ -12,22 +12,18 @@ function registerChargeCompleteSound(deps) {
|
||||
playServerAudioFile,
|
||||
} = deps;
|
||||
|
||||
subscribe('battery.unlocked', (event = {}) => {
|
||||
subscribe('battery.chargeComplete', (event = {}) => {
|
||||
const roverId = String(event?.payload?.roverId || '').trim();
|
||||
if (!roverId) return;
|
||||
|
||||
try {
|
||||
// battery.unlocked is the existing server-side definition of "done
|
||||
// charging" because it only fires after the battery manager has waited
|
||||
// through its full charge-release policy. Playing from this event avoids
|
||||
// duplicating charging thresholds or raw sensor-state guesses here.
|
||||
playServerAudioFile(roverId, DONE_CHARGING_SOUND_PATH, {
|
||||
source: 'charge-complete',
|
||||
});
|
||||
logger.info('Played charging complete sound', { roverId, filePath: DONE_CHARGING_SOUND_PATH });
|
||||
logger.info('Started charging complete sound', { roverId, filePath: DONE_CHARGING_SOUND_PATH });
|
||||
} catch (err) {
|
||||
// A missing/offline rover or unavailable ffmpeg should not affect the
|
||||
// battery manager's unlock decision. The sound is an announcement layered
|
||||
// battery manager's processing. The sound is an announcement layered
|
||||
// on top of the state transition, so failures are logged and contained.
|
||||
logger.warn('Failed to play charging complete sound', {
|
||||
roverId,
|
||||
|
||||
@@ -124,7 +124,7 @@ function grantExternalSpectatorAccessAfterAdminLogin(socket) {
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
const requestedRole = socket.handshake?.query?.role;
|
||||
const requestedRole = socket.handshake?.auth?.role || socket.handshake?.query?.role;
|
||||
/*
|
||||
Role is assigned before the browser's full identity heartbeat has completed.
|
||||
For admin-gated external spectators, fail closed here; the spectator page can
|
||||
@@ -188,4 +188,5 @@ module.exports = {
|
||||
isAdmin,
|
||||
isLockdownAdmin,
|
||||
authenticate,
|
||||
canBecomeSpectator,
|
||||
};
|
||||
|
||||
@@ -109,6 +109,7 @@ async function copyDurableTree(sourceDir, destinationDir, relativeDir = '') {
|
||||
const relativePath = path.join(relativeDir, entry.name);
|
||||
if (!relativeDir && EXCLUDED_TOP_LEVEL_NAMES.has(entry.name)) continue;
|
||||
if (!relativeDir && DATABASE_FILES.has(entry.name)) continue;
|
||||
if (relativePath === path.join('rover-learning', 'recordings')) continue;
|
||||
if (entry.isSymbolicLink()) throw new Error(`Backup cannot include symbolic link: ${relativePath}`);
|
||||
if (entry.isDirectory()) {
|
||||
skipped.push(...await copyDurableTree(sourceDir, destinationDir, relativePath));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user