diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..2d923082ace6983973b35413b043f05b9474c327
Binary files /dev/null and b/.DS_Store differ
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..240adc57d63c6ceeac5d20c26557b9a6a3c5c277
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,93 @@
+# build frontend with node
+FROM node:20-alpine AS frontend
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+COPY streaming-test-app .
+RUN \
+    if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
+    elif [ -f package-lock.json ]; then npm ci; \
+    elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
+    else echo "Lockfile not found." && exit 1; \
+    fi
+
+RUN npm run build
+
+# build backend on CUDA 
+FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 AS backend
+WORKDIR /app
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV NODE_MAJOR=20
+
+RUN apt-get update && \
+    apt-get upgrade -y && \
+    apt-get install -y --no-install-recommends \
+    git \
+    git-lfs \
+    wget \
+    curl \
+    # python build dependencies \
+    build-essential \
+    libssl-dev \
+    zlib1g-dev \
+    libbz2-dev \
+    libreadline-dev \
+    libsqlite3-dev \
+    libncursesw5-dev \
+    xz-utils \
+    tk-dev \
+    libxml2-dev \
+    libxmlsec1-dev \
+    libffi-dev \
+    liblzma-dev \
+    sox libsox-fmt-all \
+    # gradio dependencies \
+    ffmpeg \
+    # fairseq2 dependencies \
+    libjpeg8-dev \
+    libpng-dev \
+    libsndfile-dev && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
+
+USER root
+RUN ln -s /usr/lib/x86_64-linux-gnu/libsox.so.3 /usr/lib/x86_64-linux-gnu/libsox.so
+# install older versions libjpeg62-turbo and libpng15
+RUN wget http://ftp.us.debian.org/debian/pool/main/libj/libjpeg-turbo/libjpeg62-turbo_2.1.5-2_amd64.deb && \
+    dpkg -i libjpeg62-turbo_2.1.5-2_amd64.deb && \
+    rm libjpeg62-turbo_2.1.5-2_amd64.deb
+RUN wget https://master.dl.sourceforge.net/project/libpng/libpng15/1.5.30/libpng-1.5.30.tar.gz && \
+    tar -xvf libpng-1.5.30.tar.gz && cd libpng-1.5.30 && ./configure && make && make install && cd .. && rm -rf libpng-1.5.30.tar.gz libpng-1.5.30
+    
+RUN useradd -m -u 1000 user
+USER user
+ENV HOME=/home/user \
+    PATH=/home/user/.local/bin:$PATH
+WORKDIR $HOME/app
+
+RUN curl https://pyenv.run | bash
+ENV PATH=$HOME/.pyenv/shims:$HOME/.pyenv/bin:$PATH
+ARG PYTHON_VERSION=3.10.12
+RUN pyenv install $PYTHON_VERSION && \
+    pyenv global $PYTHON_VERSION && \
+    pyenv rehash && \
+    pip install --no-cache-dir -U pip setuptools wheel
+
+COPY --chown=user:user ./seamless-server ./seamless-server
+# change dir since pip needs to seed whl folder
+RUN cd seamless-server && \
+    pip install fairseq2 --pre --extra-index-url https://fair.pkg.atmeta.com/fairseq2/whl/nightly/pt2.1.1/cu118 && \
+    pip install --no-cache-dir --upgrade -r requirements.txt
+COPY --from=frontend /app/dist ./streaming-test-app/dist
+
+WORKDIR $HOME/app/seamless-server
+RUN --mount=type=secret,id=HF_TOKEN,mode=0444,required=false \ 
+    huggingface-cli login --token $(cat /run/secrets/HF_TOKEN) || echo "HF_TOKEN error" && \
+    huggingface-cli download meta-private/SeamlessExpressive pretssel_melhifigan_wm-final.pt  --local-dir ./models/Seamless/ || echo "HF_TOKEN error" && \
+    ln -s $(readlink -f models/Seamless/pretssel_melhifigan_wm-final.pt) models/Seamless/pretssel_melhifigan_wm.pt || true;
+
+USER user
+RUN ["chmod", "+x", "./run_docker.sh"]
+CMD ./run_docker.sh
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d14186c5d3c29533f7534db758481e0d7d13b719
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,17 @@
+version: '3'
+services:
+  seamless:
+    build: .
+    volumes:
+      - ./seamless-server:/home/user/app/seamless-server # for hot reload in DEV
+    ports:
+      - "80:7860"
+    environment:
+      - NODE_ENV=development
+    deploy:
+      resources:
+        reservations:
+          devices:
+            - driver: nvidia
+              count: 1
+              capabilities: [gpu]
\ No newline at end of file
diff --git a/node-server/.gitignore b/node-server/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..dc150eb5dae94eb38a7924d25cdbeedb6126cba4
--- /dev/null
+++ b/node-server/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+.env
diff --git a/node-server/package-lock.json b/node-server/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..b726a9f2dccfa3b46dfabe46324d65bc54f8c9cd
--- /dev/null
+++ b/node-server/package-lock.json
@@ -0,0 +1,2573 @@
+{
+  "name": "server",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "server",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "@deepgram/sdk": "^3.0.1",
+        "cors": "^2.8.5",
+        "crypto": "^1.0.1",
+        "dotenv": "^16.4.1",
+        "express": "^4.18.2",
+        "nodemon": "^3.0.3",
+        "socket.io": "^4.7.4"
+      },
+      "engines": {
+        "node": "v20.5.0"
+      }
+    },
+    "node_modules/@deepgram/captions": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@deepgram/captions/-/captions-1.2.0.tgz",
+      "integrity": "sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==",
+      "dependencies": {
+        "dayjs": "^1.11.10"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@deepgram/sdk": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@deepgram/sdk/-/sdk-3.0.1.tgz",
+      "integrity": "sha512-W3EomebDXsZO/eCJApX7a2yOgoTEB2O7xbQpqDWyrvQCJI7VG28w3v3fOesVnxzofIZbnBJNapXCNPmEWW16LQ==",
+      "dependencies": {
+        "@deepgram/captions": "^1.1.1",
+        "@types/websocket": "^1.0.9",
+        "cross-fetch": "^3.1.5",
+        "deepmerge": "^4.3.1",
+        "events": "^3.3.0",
+        "websocket": "^1.0.34"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
+    },
+    "node_modules/@types/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.17",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+      "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.11.8",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.8.tgz",
+      "integrity": "sha512-i7omyekpPTNdv4Jb/Rgqg0RU8YqLcNsI12quKSDkRXNfx7Wxdm6HhK1awT3xTgEkgxPn3bvnSpiEAc7a7Lpyow==",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/websocket": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz",
+      "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+      "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "on-finished": "2.4.1",
+        "qs": "6.11.0",
+        "raw-body": "2.5.1",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/bufferutil": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
+      "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "node-gyp-build": "^4.3.0"
+      },
+      "engines": {
+        "node": ">=6.14.2"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.1",
+        "set-function-length": "^1.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+    },
+    "node_modules/cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/cross-fetch": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+      "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+      "dependencies": {
+        "node-fetch": "^2.6.12"
+      }
+    },
+    "node_modules/crypto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+      "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
+      "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
+    },
+    "node_modules/d": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "dependencies": {
+        "es5-ext": "^0.10.50",
+        "type": "^1.0.1"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.10",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/define-data-property": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.1",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.4.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
+      "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/motdotla/dotenv?sponsor=1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+    },
+    "node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/engine.io": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
+      "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==",
+      "dependencies": {
+        "@types/cookie": "^0.4.1",
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.11.0"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
+      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io/node_modules/cookie": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+      "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/es5-ext": {
+      "version": "0.10.62",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
+      "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.3",
+        "next-tick": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "node_modules/es6-symbol": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "dependencies": {
+        "d": "^1.0.1",
+        "ext": "^1.1.2"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.18.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.5.0",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.2.0",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.11.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.18.0",
+        "serve-static": "1.15.0",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/ext": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+      "dependencies": {
+        "type": "^2.7.2"
+      }
+    },
+    "node_modules/ext/node_modules/type": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+      "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
+    },
+    "node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+      "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+      "dependencies": {
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "dependencies": {
+        "get-intrinsic": "^1.1.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ignore-by-default": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+      "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
+    },
+    "node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
+      "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==",
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/nodemon": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz",
+      "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==",
+      "dependencies": {
+        "chokidar": "^3.5.2",
+        "debug": "^4",
+        "ignore-by-default": "^1.0.1",
+        "minimatch": "^3.1.2",
+        "pstree.remy": "^1.1.8",
+        "semver": "^7.5.3",
+        "simple-update-notifier": "^2.0.0",
+        "supports-color": "^5.5.0",
+        "touch": "^3.1.0",
+        "undefsafe": "^2.0.5"
+      },
+      "bin": {
+        "nodemon": "bin/nodemon.js"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/nodemon"
+      }
+    },
+    "node_modules/nodemon/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/nodemon/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/nopt": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+      "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
+      "dependencies": {
+        "abbrev": "1"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+      "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/pstree.remy": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+      "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+    },
+    "node_modules/qs": {
+      "version": "6.11.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+      "dependencies": {
+        "side-channel": "^1.0.4"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+      "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/semver": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/serve-static": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+      "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+      "dependencies": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.18.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/set-function-length": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
+      "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+      "dependencies": {
+        "define-data-property": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.2",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "node_modules/side-channel": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "dependencies": {
+        "call-bind": "^1.0.0",
+        "get-intrinsic": "^1.0.2",
+        "object-inspect": "^1.9.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/simple-update-notifier": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+      "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/socket.io": {
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz",
+      "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.3.2",
+        "engine.io": "~6.5.2",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+      "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+      "dependencies": {
+        "ws": "~8.11.0"
+      }
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/socket.io/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/touch": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+      "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+      "dependencies": {
+        "nopt": "~1.0.10"
+      },
+      "bin": {
+        "nodetouch": "bin/nodetouch.js"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
+    "node_modules/type": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typedarray-to-buffer": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+      "dependencies": {
+        "is-typedarray": "^1.0.0"
+      }
+    },
+    "node_modules/undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utf-8-validate": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
+      "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "node-gyp-build": "^4.3.0"
+      },
+      "engines": {
+        "node": ">=6.14.2"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "node_modules/websocket": {
+      "version": "1.0.34",
+      "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
+      "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==",
+      "dependencies": {
+        "bufferutil": "^4.0.1",
+        "debug": "^2.2.0",
+        "es5-ext": "^0.10.50",
+        "typedarray-to-buffer": "^3.1.5",
+        "utf-8-validate": "^5.0.2",
+        "yaeti": "^0.0.6"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yaeti": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
+      "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
+      "engines": {
+        "node": ">=0.10.32"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    }
+  },
+  "dependencies": {
+    "@deepgram/captions": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@deepgram/captions/-/captions-1.2.0.tgz",
+      "integrity": "sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==",
+      "requires": {
+        "dayjs": "^1.11.10"
+      }
+    },
+    "@deepgram/sdk": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@deepgram/sdk/-/sdk-3.0.1.tgz",
+      "integrity": "sha512-W3EomebDXsZO/eCJApX7a2yOgoTEB2O7xbQpqDWyrvQCJI7VG28w3v3fOesVnxzofIZbnBJNapXCNPmEWW16LQ==",
+      "requires": {
+        "@deepgram/captions": "^1.1.1",
+        "@types/websocket": "^1.0.9",
+        "cross-fetch": "^3.1.5",
+        "deepmerge": "^4.3.1",
+        "events": "^3.3.0",
+        "websocket": "^1.0.34"
+      }
+    },
+    "@socket.io/component-emitter": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
+    },
+    "@types/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+    },
+    "@types/cors": {
+      "version": "2.8.17",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+      "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/node": {
+      "version": "20.11.8",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.8.tgz",
+      "integrity": "sha512-i7omyekpPTNdv4Jb/Rgqg0RU8YqLcNsI12quKSDkRXNfx7Wxdm6HhK1awT3xTgEkgxPn3bvnSpiEAc7a7Lpyow==",
+      "requires": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "@types/websocket": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz",
+      "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+    },
+    "accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "requires": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      }
+    },
+    "anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
+    },
+    "binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
+    },
+    "body-parser": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+      "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+      "requires": {
+        "bytes": "3.1.2",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "on-finished": "2.4.1",
+        "qs": "6.11.0",
+        "raw-body": "2.5.1",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "bufferutil": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
+      "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
+      "requires": {
+        "node-gyp-build": "^4.3.0"
+      }
+    },
+    "bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
+    },
+    "call-bind": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "requires": {
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.1",
+        "set-function-length": "^1.1.1"
+      }
+    },
+    "chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "requires": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "fsevents": "~2.3.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "requires": {
+        "safe-buffer": "5.2.1"
+      }
+    },
+    "content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
+    },
+    "cookie": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+    },
+    "cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "requires": {
+        "object-assign": "^4",
+        "vary": "^1"
+      }
+    },
+    "cross-fetch": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+      "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+      "requires": {
+        "node-fetch": "^2.6.12"
+      }
+    },
+    "crypto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+      "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
+    },
+    "d": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "requires": {
+        "es5-ext": "^0.10.50",
+        "type": "^1.0.1"
+      }
+    },
+    "dayjs": {
+      "version": "1.11.10",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
+    },
+    "define-data-property": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "requires": {
+        "get-intrinsic": "^1.2.1",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.0"
+      }
+    },
+    "depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+    },
+    "destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
+    },
+    "dotenv": {
+      "version": "16.4.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
+      "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ=="
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
+    },
+    "engine.io": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
+      "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==",
+      "requires": {
+        "@types/cookie": "^0.4.1",
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.11.0"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+          "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
+        },
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
+      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
+    },
+    "es5-ext": {
+      "version": "0.10.62",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
+      "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
+      "requires": {
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.3",
+        "next-tick": "^1.1.0"
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "requires": {
+        "d": "^1.0.1",
+        "ext": "^1.1.2"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
+    },
+    "events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
+    },
+    "express": {
+      "version": "4.18.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+      "requires": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.5.0",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.2.0",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.11.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.18.0",
+        "serve-static": "1.15.0",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      }
+    },
+    "ext": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+      "requires": {
+        "type": "^2.7.2"
+      },
+      "dependencies": {
+        "type": {
+          "version": "2.7.2",
+          "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+          "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
+        }
+      }
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "finalhandler": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      }
+    },
+    "forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
+    },
+    "fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "optional": true
+    },
+    "function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "get-intrinsic": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+      "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+      "requires": {
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "requires": {
+        "get-intrinsic": "^1.1.3"
+      }
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
+    },
+    "has-property-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "requires": {
+        "get-intrinsic": "^1.2.2"
+      }
+    },
+    "has-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
+    },
+    "has-symbols": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+    },
+    "hasown": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
+    "http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "requires": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ignore-by-default": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+      "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+    },
+    "is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+    },
+    "is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
+    },
+    "lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "requires": {
+        "yallist": "^4.0.0"
+      }
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
+    "minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+    },
+    "negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
+    },
+    "next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+    },
+    "node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "requires": {
+        "whatwg-url": "^5.0.0"
+      }
+    },
+    "node-gyp-build": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
+      "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og=="
+    },
+    "nodemon": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz",
+      "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==",
+      "requires": {
+        "chokidar": "^3.5.2",
+        "debug": "^4",
+        "ignore-by-default": "^1.0.1",
+        "minimatch": "^3.1.2",
+        "pstree.remy": "^1.1.8",
+        "semver": "^7.5.3",
+        "simple-update-notifier": "^2.0.0",
+        "supports-color": "^5.5.0",
+        "touch": "^3.1.0",
+        "undefsafe": "^2.0.5"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "nopt": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+      "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
+      "requires": {
+        "abbrev": "1"
+      }
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+    },
+    "object-inspect": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+      "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
+    },
+    "on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
+    },
+    "picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+    },
+    "proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "requires": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      }
+    },
+    "pstree.remy": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+      "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+    },
+    "qs": {
+      "version": "6.11.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+      "requires": {
+        "side-channel": "^1.0.4"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+    },
+    "raw-body": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+      "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+      "requires": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      }
+    },
+    "readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "requires": {
+        "picomatch": "^2.2.1"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "semver": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "requires": {
+        "lru-cache": "^6.0.0"
+      }
+    },
+    "send": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "2.0.1"
+      },
+      "dependencies": {
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
+    "serve-static": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+      "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.18.0"
+      }
+    },
+    "set-function-length": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
+      "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+      "requires": {
+        "define-data-property": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.2",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.1"
+      }
+    },
+    "setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "side-channel": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "requires": {
+        "call-bind": "^1.0.0",
+        "get-intrinsic": "^1.0.2",
+        "object-inspect": "^1.9.0"
+      }
+    },
+    "simple-update-notifier": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+      "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+      "requires": {
+        "semver": "^7.5.3"
+      }
+    },
+    "socket.io": {
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz",
+      "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==",
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.3.2",
+        "engine.io": "~6.5.2",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+      "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+      "requires": {
+        "ws": "~8.11.0"
+      }
+    },
+    "socket.io-parser": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "requires": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+    },
+    "touch": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+      "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+      "requires": {
+        "nopt": "~1.0.10"
+      }
+    },
+    "tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
+    "type": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+    },
+    "type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      }
+    },
+    "typedarray-to-buffer": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+      "requires": {
+        "is-typedarray": "^1.0.0"
+      }
+    },
+    "undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+    },
+    "undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
+    },
+    "utf-8-validate": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
+      "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
+      "requires": {
+        "node-gyp-build": "^4.3.0"
+      }
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
+    },
+    "webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "websocket": {
+      "version": "1.0.34",
+      "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
+      "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==",
+      "requires": {
+        "bufferutil": "^4.0.1",
+        "debug": "^2.2.0",
+        "es5-ext": "^0.10.50",
+        "typedarray-to-buffer": "^3.1.5",
+        "utf-8-validate": "^5.0.2",
+        "yaeti": "^0.0.6"
+      }
+    },
+    "whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "requires": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "ws": {
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "requires": {}
+    },
+    "yaeti": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
+      "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="
+    },
+    "yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    }
+  }
+}
diff --git a/node-server/package.json b/node-server/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d3de57e531ce2ad0426ebfb7e5c7dc960e6e38f4
--- /dev/null
+++ b/node-server/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "server",
+  "version": "1.0.0",
+  "description": "",
+  "main": "server.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "node server.js",
+    "dev": "nodemon index.js"
+  },
+  "engines": {
+    "node": "v20.5.0"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@deepgram/sdk": "^3.0.1",
+    "cors": "^2.8.5",
+    "crypto": "^1.0.1",
+    "dotenv": "^16.4.1",
+    "express": "^4.18.2",
+    "nodemon": "^3.0.3",
+    "socket.io": "^4.7.4"
+  }
+}
diff --git a/node-server/server.js b/node-server/server.js
new file mode 100644
index 0000000000000000000000000000000000000000..85ab4d027be45ffd46bea4cee9c0781b722b214a
--- /dev/null
+++ b/node-server/server.js
@@ -0,0 +1,33 @@
+require("dotenv").config();
+const express = require("express");
+const cors = require("cors");
+const app = express();
+const http = require("http").Server(app);
+const initializeWebSocket = require("./websocket");
+
+// TODO redis store
+const io = require("socket.io")(http, {
+  cors: {
+    origin: "http://localhost:5173",
+    methods: ["GET", "POST"],
+  },
+});
+
+initializeWebSocket(io);
+
+app.use(cors({ credentials: false, origin: "http://localhost:5173" }));
+
+app.use(express.json());
+
+app.use((req, _, next) => {
+  req.io = io;
+  next();
+});
+
+
+app.get("/", (req, res) => res.send("worked"))
+
+const PORT = process.env.PORT || 3002;
+http.listen(PORT, () => {
+  console.log(`Server listening at http://localhost:${PORT}`);
+});
diff --git a/node-server/transcription-client.js b/node-server/transcription-client.js
new file mode 100644
index 0000000000000000000000000000000000000000..2331e7ac89bb73e45167999008ba11b5501f0157
--- /dev/null
+++ b/node-server/transcription-client.js
@@ -0,0 +1,172 @@
+const { createClient, LiveTranscriptionEvents } = require("@deepgram/sdk");
+const EventEmitter = require("events");
+const crypto = require("crypto");
+
+class TranscriptionClient extends EventEmitter {
+  constructor() {
+    super();
+    this.deepgramStream = null;
+    this.deepgramSessionId = null; 
+    this.currentTranscript = "";
+    this.currentDiarization = {};
+    this.releaseTimeout = null;
+    this.killTimeout = null;
+    this.releaseThresholdMS = 4000;
+    this.killThresholdMS = 1000 * 60 * 2;
+    this.diarize = false;
+    this.speakerLabels = {};
+  }
+
+  startTranscriptionStream(language) {
+    console.log("started deepgram");
+    const localSessionId = crypto.randomUUID();
+    this.deepgramSessionId = localSessionId;
+    const deepgram = createClient(process.env.DEEPGRAM_API_KEY);
+    this.deepgramStream = deepgram.listen.live({
+      model: "nova-2",
+      punctuate: true,
+      language,
+      interim_results: true,
+      diarize: this.diarize,
+      smart_format: true,
+      endpointing: "2",
+    });
+
+    this.deepgramStream.on(LiveTranscriptionEvents.Error, (err) => {
+      console.log("Deepgram error: ", err);
+    });
+
+    this.deepgramStream.on(LiveTranscriptionEvents.Warning, (err) => {
+      console.log("Deepgram error: ", err);
+    });
+
+    this.deepgramStream.on(LiveTranscriptionEvents.Open, () => {
+      this.resetKillTimeout();
+
+      this.deepgramStream.on(
+        LiveTranscriptionEvents.Transcript,
+        async (data) => {
+          try {
+            const response = data.channel.alternatives[0];
+            const text = response?.transcript || "";
+            if (text.length > 1) {
+              clearTimeout(this.releaseTimeout);
+              this.releaseTimeout = setTimeout(() => {
+                this.releaseTranslations(true);
+              }, this.releaseThresholdMS);
+              this.resetKillTimeout();
+            }
+
+            // important not to translate interim results
+            if (response.transcript && data.is_final) {
+              // console.log(response.transcript);
+              const words = data.channel?.alternatives[0]?.words || [];
+              words.forEach(({ punctuated_word, speaker, start, end }) => {
+                if (!this.currentDiarization[speaker])
+                  this.currentDiarization[speaker] = "";
+                this.currentDiarization[speaker] += " " + punctuated_word;
+              });
+              this.emit("transcript", text)
+              this.currentTranscript += " " + text;
+              this.releaseTranslations();
+              // this.fullTranscript += " " + this.currentTranscript;
+            }
+          } catch (err) {
+            console.log(
+              "TranscribeTranslate.LiveTranscriptionEvents.Transcript:",
+              err
+            );
+          }
+        }
+      );
+    });
+    return this.deepgramSessionId;
+  }
+
+  resetKillTimeout = () => {
+    clearTimeout(this.killTimeout);
+    this.killTimeout = setTimeout(
+      () => this.endTranscriptionStream(),
+      this.killThresholdMS
+    );
+  };
+
+  releaseTranslations = async (triggeredByPause = false) => {
+    try {
+      let segment = "";
+      let speaker = null;
+      if (this.diarize) {
+        const processedSpeakers = Object.entries(this.currentDiarization).map(
+          ([speaker, transcript]) => ({
+            ...this.checkShouldSegment(transcript, triggeredByPause ? 5 : 50),
+            speaker,
+          })
+        );
+        const chosen = processedSpeakers.find((s) => s.canRelease);
+        if (!chosen) return;
+        this.currentDiarization = { [chosen.speaker]: chosen.secondPart };
+        segment = chosen.firstPart;
+        speaker = this.getSpeakerLabel(chosen.speaker);
+      } else {
+        const { canRelease, firstPart, secondPart } = this.checkShouldSegment(
+          this.currentTranscript,
+          triggeredByPause ? 5 : 50
+        );
+        if (!canRelease) return;
+        this.currentTranscript = secondPart;
+        segment = firstPart;
+      }
+
+      // translate segment
+      this.emit("translation", segment)
+
+   
+      this.lastEmittedSpeaker = speaker;
+    } catch (err) {
+      console.log("TranscribeTranslate.releaseTranslations:", err);
+    }
+  };
+
+  endTranscriptionStream() {
+    try {
+      clearTimeout(this.releaseTimeout);
+      clearTimeout(this.killTimeout);
+      if (!this.deepgramStream) return;
+      this.deepgramStream.finish();
+      this.deepgramStream = null;
+      this.currentTranscript = "";
+    } catch (err) {
+      console.log("Failed to end deepgram stream", err);
+    }
+  }
+
+  checkShouldSegment = (str, minCharLimit = 25) => {
+    let firstPart = "";
+    let secondPart = "";
+    const punct = new Set([".", "!", "?", "。", "۔"]);
+    for (let i = 0; i < str.length; i += 1) {
+      const char = str[i];
+      if (i > minCharLimit) {
+        if (punct.has(char)) {
+          firstPart = str.slice(0, i + 1);
+          secondPart = str.slice(i + 1);
+        }
+      }
+    }
+  
+    return { canRelease: !!firstPart.length, firstPart, secondPart };
+  };
+
+  send(payload) {
+    try {
+        if (!this.deepgramStream) return;
+        if (this.deepgramStream.getReadyState() === 1) {
+          this.deepgramStream.send(payload);
+        }
+      } catch (err) {
+        console.log("Failed to start deepgram stream", err);
+      }
+  }
+}
+
+module.exports = TranscriptionClient;
diff --git a/node-server/websocket.js b/node-server/websocket.js
new file mode 100644
index 0000000000000000000000000000000000000000..989e8d528024776f0bc5a08fdd0087c52abdcb43
--- /dev/null
+++ b/node-server/websocket.js
@@ -0,0 +1,30 @@
+const TranscriptClient = require("./transcription-client");
+
+// TODO remove x seconds after host left (incase reconnect)
+const initializeWebSocket = (io) => {
+  io.on("connection", (socket) => {
+    console.log(`connection made (${socket.id})`);
+    const transcriptClient = new TranscriptClient();
+
+    transcriptClient.on("translation", (result) => {
+      console.log(result)
+      io.to(socket.id).emit("translation", result)
+    })
+
+    socket.on('configure_stream', ({language}) => {
+      transcriptClient.startTranscriptionStream("en-US")
+    })
+
+    socket.on('incoming_audio', (data) => {
+      transcriptClient.send(data)
+    })
+
+    socket.on("disconnect", () => {
+      transcriptClient.endTranscriptionStream()
+    });
+  });
+
+  return io;
+};
+
+module.exports = initializeWebSocket;
diff --git a/seamless-server/.DS_Store b/seamless-server/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..feca8613504bcd7430524202412f605a970dd548
Binary files /dev/null and b/seamless-server/.DS_Store differ
diff --git a/seamless-server/.gitignore b/seamless-server/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..55083f72b82e3ec70cd12b71c163227d9351196a
--- /dev/null
+++ b/seamless-server/.gitignore
@@ -0,0 +1,5 @@
+__pycache__/
+src/__pycache__/
+debug/
+.vscode/
+.env
\ No newline at end of file
diff --git a/seamless-server/models/Seamless/vad_s2st_sc_24khz_main.yaml b/seamless-server/models/Seamless/vad_s2st_sc_24khz_main.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f7dfef9be652d703d7c6c187d13113a71b0127d4
--- /dev/null
+++ b/seamless-server/models/Seamless/vad_s2st_sc_24khz_main.yaml
@@ -0,0 +1,25 @@
+agent_class: seamless_communication.streaming.agents.seamless_s2st.SeamlessS2STDualVocoderVADAgent
+monotonic_decoder_model_name: seamless_streaming_monotonic_decoder
+unity_model_name: seamless_streaming_unity
+sentencepiece_model: spm_256k_nllb100.model
+
+task: s2st
+tgt_lang: "eng"
+min_unit_chunk_size: 50
+decision_threshold: 0.7
+no_early_stop: True
+block_ngrams: True
+vocoder_name: vocoder_v2
+expr_vocoder_name: vocoder_pretssel
+gated_model_dir: .
+expr_vocoder_gain: 3.0
+upstream_idx: 1
+wav2vec_yaml: wav2vec.yaml
+min_starting_wait_w2vbert: 192
+
+config_yaml: cfg_fbank_u2t.yaml
+upstream_idx: 1
+detokenize_only: True
+device: cuda:0
+max_len_a: 0
+max_len_b: 1000
diff --git a/seamless-server/models/SeamlessStreaming/vad_s2st_sc_main.yaml b/seamless-server/models/SeamlessStreaming/vad_s2st_sc_main.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1ffba98b76f9cff0c21095e160670ffc003c227c
--- /dev/null
+++ b/seamless-server/models/SeamlessStreaming/vad_s2st_sc_main.yaml
@@ -0,0 +1,21 @@
+agent_class: seamless_communication.streaming.agents.seamless_streaming_s2st.SeamlessStreamingS2STJointVADAgent
+monotonic_decoder_model_name: seamless_streaming_monotonic_decoder
+unity_model_name: seamless_streaming_unity
+sentencepiece_model: spm_256k_nllb100.model
+
+task: s2st
+tgt_lang: "eng"
+min_unit_chunk_size: 50
+decision_threshold: 0.7
+no_early_stop: True
+block_ngrams: True
+vocoder_name: vocoder_v2
+wav2vec_yaml: wav2vec.yaml
+min_starting_wait_w2vbert: 192
+
+config_yaml: cfg_fbank_u2t.yaml
+upstream_idx: 1
+detokenize_only: True
+device: cuda:0
+max_len_a: 0
+max_len_b: 1000
diff --git a/seamless-server/old_server.py b/seamless-server/old_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..217fd4e59d9b0a5d0de4ed706ad6f6fbf8c20bd0
--- /dev/null
+++ b/seamless-server/old_server.py
@@ -0,0 +1,874 @@
+from operator import itemgetter
+import os
+from typing import Any, Optional, Tuple, Dict, TypedDict
+from urllib import parse
+from uuid import uuid4
+from pprint import pformat
+import socketio
+import time
+import random
+import string
+import logging
+from starlette.applications import Starlette
+from starlette.routing import Mount, Route
+from starlette.staticfiles import StaticFiles
+from dotenv import load_dotenv
+
+load_dotenv()
+
+from src.auth import google_auth_check
+from src.room import Room, Member
+from src.context import ContextManager
+from src.transcriber import Transcriber
+
+from src.simuleval_agent_directory import NoAvailableAgentException
+from src.simuleval_agent_directory import SimulevalAgentDirectory
+from src.simuleval_transcoder import SimulevalTranscoder
+from src.transcoder_helpers import get_transcoder_output_events
+from src.logging import initialize_logger
+
+DEBUG = True
+ALL_ROOM_ID = "ALL"
+ROOM_ID_USABLE_CHARACTERS = string.ascii_uppercase
+ROOM_ID_LENGTH = 4
+ROOM_LISTENERS_SUFFIX = "_listeners"
+ROOM_SPEAKERS_SUFFIX = "_speakers"
+ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME = "remove_server_lock"
+
+logger = initialize_logger("socketio_server_pubsub", level=logging.WARNING)
+
+print("=" * 20 + " ⭐️ Starting Server... ⭐️ " + "=" * 20)
+
+CLIENT_BUILD_PATH = "../streaming-test-app/dist/"
+static_files = {
+    "/": CLIENT_BUILD_PATH,
+    "/assets/seamless-db6a2555.svg": {
+        "filename": CLIENT_BUILD_PATH + "assets/seamless-db6a2555.svg",
+        "content_type": "image/svg+xml",
+    },
+}
+
+# sio is the main socket.io entrypoint
+sio = socketio.AsyncServer(
+    async_mode="asgi",
+    cors_allowed_origins="*",
+    logger=logger,
+    # engineio_logger=logger,
+)
+# sio.logger.setLevel(logging.DEBUG)
+socketio_app = socketio.ASGIApp(sio)
+
+app_routes = [
+    Mount("/ws", app=socketio_app),  # Mount Socket.IO server under /app
+    Mount(
+        "/", app=StaticFiles(directory=CLIENT_BUILD_PATH, html=True)
+    ),  # Serve static files from root
+]
+app = Starlette(debug=True, routes=app_routes)
+
+# rooms is indexed by room_id
+rooms: Dict[str, Room] = {}
+
+
+class MemberDirectoryObject(TypedDict):
+    room: Room
+    member_object: Member
+
+
+# member_directory is indexed by client_id
+# NOTE: client_id is really "client session id", meaning that it is unique to a single browser session.
+# If a user opens a new tab, they will have a different client_id and can join another room, join
+# the same room with different roles, etc.
+# NOTE: For a long-running production server we would want to clean up members after a certain timeout
+# but for this limited application we can just keep them around
+member_directory: Dict[str, MemberDirectoryObject] = {}
+
+
+class ServerLock(TypedDict):
+    name: str
+    client_id: str
+    member_object: Member
+
+
+SINGLE_USER = os.environ.get("SINGLE_USER")
+
+if os.environ.get("LOCK_SERVER_COMPLETELY", "0") == "1":
+    logger.info("LOCK_SERVER_COMPLETELY is set. Server will be locked on startup.")
+if SINGLE_USER == "1":
+    logger.info(
+        f"SINGLE_USER mode is set. Server will only allow one speaker or listener at a time."
+    )
+dummy_server_lock_member_object = Member(
+    client_id="seamless_user", session_id="dummy", name="Seamless User"
+)
+# Normally this would be an actual transcoder, but it's fine putting True here since currently we only check for the presence of the transcoder
+dummy_server_lock_member_object.transcoder = True
+server_lock: Optional[ServerLock] = (
+    {
+        "name": "Seamless User",
+        "client_id": "seamless_user",
+        "member_object": dummy_server_lock_member_object,
+    }
+    if os.environ.get("LOCK_SERVER_COMPLETELY", "0") == "1"
+    else None
+)
+
+server_id = str(uuid4())
+
+# Specify specific models to load (some environments have issues loading multiple models)
+# See AgentWithInfo with JSON format details.
+models_override = os.environ.get("MODELS_OVERRIDE")
+
+available_agents = SimulevalAgentDirectory()
+logger.info("Building and adding agents...")
+if models_override is not None:
+    logger.info(f"MODELS_OVERRIDE supplied from env vars: {models_override}")
+available_agents.build_and_add_agents(models_override)
+
+agents_capabilities_for_json = available_agents.get_agents_capabilities_list_for_json()
+
+
+def catch_and_log_exceptions_for_sio_event_handlers(func):
+    # wrapper should have the same signature as the original function
+    async def catch_exception_wrapper(*args, **kwargs):
+        try:
+            return await func(*args, **kwargs)
+        except Exception as e:
+            message = f"[app_pubsub] Caught exception in '{func.__name__}' event handler:\n\n{e}"
+            logger.exception(message, stack_info=True)
+
+            try:
+                exception_data = {
+                    "message": message,
+                    "timeEpochMs": int(time.time() * 1000),
+                }
+
+                try:
+                    # Let's try to add as much useful metadata as possible to the server_exception event
+                    sid = args[0]
+                    if isinstance(sid, str) and len(sid) > 0:
+                        session_data = await get_session_data(sid)
+                        if session_data:
+                            client_id = session_data.get("client_id")
+                            member = session_data.get("member_object")
+                            room = session_data.get("room_object")
+
+                            exception_data["room"] = str(room)
+                            exception_data["member"] = str(member)
+                            exception_data["clientID"] = str(client_id)
+                except Exception as inner_e:
+                    # We expect there will be times when clientID or other values aren't present, so just log this as a warning
+                    logger.warn(
+                        f"[app_pubsub] Caught exception while trying add additional_data to server_exception:\n\n{inner_e}"
+                    )
+
+                # For now let's emit this to all clients. We ultimatley may want to emit it just to the room it's happening in.
+                await sio.emit("server_exception", exception_data)
+            except Exception as inner_e:
+                logger.exception(
+                    f"[app_pubsub] Caught exception while trying to emit server_exception event:\n{inner_e}"
+                )
+
+            # Re-raise the exception so it's handled normally by the server
+            raise e
+
+    # Set the name of the wrapper to the name of the original function so that the socketio server can associate it with the right event
+    catch_exception_wrapper.__name__ = func.__name__
+    return catch_exception_wrapper
+
+
+async def emit_room_state_update(room):
+    await sio.emit(
+        "room_state_update",
+        room.to_json(),
+        room=room.room_id,
+    )
+
+
+async def emit_server_state_update():
+    room_statuses = {
+        room_id: room.get_room_status_dict() for room_id, room in rooms.items()
+    }
+    total_active_connections = sum(
+        [room_status["activeConnections"] for room_status in room_statuses.values()]
+    )
+    total_active_transcoders = sum(
+        [room_status["activeTranscoders"] for room_status in room_statuses.values()]
+    )
+    logger.info(
+        f"[Server Status]: {total_active_connections} active connections (in rooms); {total_active_transcoders} active transcoders"
+    )
+    logger.info(f"[Server Status]: server_lock={server_lock}")
+    server_lock_object_for_js = (
+        {
+            "name": server_lock.get("name"),
+            "clientID": server_lock.get("client_id"),
+            "isActive": server_lock.get("member_object")
+            and server_lock.get("member_object").transcoder is not None,
+        }
+        if server_lock
+        else None
+    )
+    await sio.emit(
+        "server_state_update",
+        {
+            "statusByRoom": room_statuses,
+            "totalActiveConnections": total_active_connections,
+            "totalActiveTranscoders": total_active_transcoders,
+            "agentsCapabilities": agents_capabilities_for_json,
+            "serverLock": server_lock_object_for_js,
+        },
+        room=ALL_ROOM_ID,
+    )
+
+
+async def get_session_data(sid):
+    session = await sio.get_session(sid)
+    # It seems like if the session has not been set that get_session may return None, so let's provide a fallback empty dictionary here
+    return session or {}
+
+
+async def set_session_data(
+    sid, client_id, room_id, room_object, member_object, context_obj, transcriber
+):
+    await sio.save_session(
+        sid,
+        {
+            "client_id": client_id,
+            "room_id": room_id,
+            "room_object": room_object,
+            "member_object": member_object,
+            "context_obj": context_obj,
+            "transcriber": transcriber,
+        },
+    )
+
+
+def get_random_room_id():
+    return "".join(random.choices(ROOM_ID_USABLE_CHARACTERS, k=ROOM_ID_LENGTH))
+
+
+def get_random_unused_room_id():
+    room_id = get_random_room_id()
+    while room_id in rooms:
+        room_id = get_random_room_id()
+    return room_id
+
+
+###############################################
+# Socket.io Basic Event Handlers
+###############################################
+
+
+@sio.on("connect")
+@catch_and_log_exceptions_for_sio_event_handlers
+async def connect(sid, environ):
+    logger.info(f"📥 [event: connected] sid={sid}")
+
+    # TODO: Sanitize/validate query param input
+    query_params = dict(parse.parse_qsl(environ["QUERY_STRING"]))
+    client_id = query_params.get("clientID")
+    token = query_params.get("token")
+
+    if google_auth_check(token) is None:
+        await sio.emit("auth_error", "Not authenticated", to=sid)
+        logger.info("Invalid auth token, Disconnecting...")
+        await sio.disconnect(sid)
+        return
+
+    logger.debug(f"query_params:\n{pformat(query_params)}")
+
+    if client_id is None:
+        logger.info("No clientID provided. Disconnecting...")
+        await sio.disconnect(sid)
+        return
+
+    # On reconnect we need to rejoin rooms and reset session data
+    if member_directory.get(client_id):
+        room = member_directory[client_id].get("room")
+        room_id = room.room_id
+        # Note: We could also get this from room.members[client_id]
+        member = member_directory[client_id].get("member_object")
+        context = member_directory[client_id].get("context_obj")
+        transcriber = member_directory[client_id].get("transcriber")
+        member.connection_status = "connected"
+        member.session_id = sid
+
+        logger.info(
+            f"[event: connect] {member} reconnected. Attempting to re-add them to socketio rooms and reset session data."
+        )
+
+        if room is None or member is None:
+            logger.error(
+                f"[event: connect] {client_id} is reconnecting, but room or member is None. This should not happen."
+            )
+            await sio.disconnect(sid)
+            return
+
+        sio.enter_room(sid, room_id)
+        sio.enter_room(sid, ALL_ROOM_ID)
+
+        if client_id in room.listeners:
+            sio.enter_room(sid, f"{room_id}{ROOM_LISTENERS_SUFFIX}")
+        if client_id in room.speakers:
+            sio.enter_room(sid, f"{room_id}{ROOM_SPEAKERS_SUFFIX}")
+
+        # Save the room_id to the socketio client session
+        await set_session_data(
+            sid,
+            client_id=client_id,
+            room_id=room.room_id,
+            room_object=room,
+            member_object=member,
+            context_obj=context,
+            transcriber=transcriber,
+        )
+        await emit_room_state_update(room)
+    else:
+        # Save the client id to the socketio client session
+        await set_session_data(
+            sid,
+            client_id=client_id,
+            room_id=None,
+            room_object=None,
+            member_object=None,
+            context_obj=None,
+            transcriber=None,
+        )
+
+    await sio.emit("server_id", server_id, to=sid)
+    await emit_server_state_update()
+
+
+@sio.event
+@catch_and_log_exceptions_for_sio_event_handlers
+async def disconnect(sid):
+    global server_lock
+    session_data = await get_session_data(sid)
+
+    client_id = None
+    member = None
+    room = None
+
+    if session_data:
+        client_id = session_data.get("client_id")
+        member = session_data.get("member_object")
+        room = session_data.get("room_object")
+
+    logger.info(
+        f"[event: disconnect][{room or 'NOT_IN_ROOM'}] member: {member or 'NO_MEMBER_OBJECT'} disconnected"
+    )
+
+    # Release the lock if this is the client that holds the current server lock
+    if server_lock and server_lock.get("client_id") == client_id:
+        server_lock = None
+
+    if member:
+        member.connection_status = "disconnected"
+
+        if member.transcoder:
+            member.transcoder.close = True
+            member.transcoder = None
+            member.requested_output_type = None
+
+        if room:
+            logger.info(
+                f"[event: disconnect] {member} disconnected from room {room.room_id}"
+            )
+            await emit_room_state_update(room)
+        else:
+            logger.info(
+                f"[event: disconnect] {member} disconnected, but no room object present. This should not happen."
+            )
+    else:
+        logger.info(
+            f"[event: disconnect] client_id {client_id or 'NO_CLIENT_ID'} with sid {sid} in rooms {str(sio.rooms(sid))} disconnected"
+        )
+
+    await emit_server_state_update()
+
+
+@sio.on("*")
+async def catch_all(event, sid, data):
+    logger.info(f"[unhandled event: {event}] sid={sid} data={data}")
+
+
+###############################################
+# Socket.io Streaming Event handlers
+###############################################
+
+
+@sio.on("join_room")
+@catch_and_log_exceptions_for_sio_event_handlers
+async def join_room(sid, client_id, room_id_from_client, config_dict):
+    global server_lock
+
+    args = {
+        "sid": sid,
+        "client_id": client_id,
+        "room_id": room_id_from_client,
+        "config_dict": config_dict,
+    }
+    logger.info(f"[event: join_room] {args}")
+    session_data = await get_session_data(sid)
+
+    logger.info(f"session_data: {session_data}")
+
+    room_id = room_id_from_client
+    if room_id is None:
+        room_id = get_random_unused_room_id()
+        logger.info(
+            f"No room_id provided. Generating a random, unused room_id: {room_id}"
+        )
+
+    # Create the room if it doesn't already exist
+    if room_id not in rooms:
+        rooms[room_id] = Room(room_id)
+
+    room = rooms[room_id]
+
+    member = None
+
+    name = "[NO_NAME]"
+
+    context = ContextManager()
+
+    transcriber = Transcriber()
+
+    # If the client is reconnecting use their existing member object. Otherwise create a new one.
+    if client_id in room.members:
+        member = room.members[client_id]
+        logger.info(f"{member} is rejoining room {room_id}.")
+    else:
+        member_number = len(room.members) + 1
+        name = f"Member {member_number}"
+        member = Member(
+            client_id=client_id,
+            session_id=sid,
+            name=name,
+        )
+        allow_user = check_and_lock_single_user(client_id, member)
+        if not allow_user:
+            logger.error(
+                f"In SINGLE_USER mode we only allow one user at a time. Ignoring request to configure stream from client {client_id}."
+            )
+            return {"status": "error", "message": "max_users"}
+
+        logger.info(f"Created a new Member object: {member}")
+        logger.info(f"Adding {member} to room {room_id}")
+        room.members[client_id] = member
+
+    # Also add them to the member directory
+    member_directory[client_id] = {"room": room, "member_object": member}
+
+    # Join the socketio room, which enables broadcasting to all members of the room
+    sio.enter_room(sid, room_id)
+    # Join the room for all clients
+    sio.enter_room(sid, ALL_ROOM_ID)
+
+    if "listener" in config_dict["roles"]:
+        sio.enter_room(sid, f"{room_id}{ROOM_LISTENERS_SUFFIX}")
+        if client_id not in room.listeners:
+            room.listeners.append(client_id)
+    else:
+        sio.leave_room(sid, f"{room_id}{ROOM_LISTENERS_SUFFIX}")
+        room.listeners = [
+            listener_id for listener_id in room.listeners if listener_id != client_id
+        ]
+
+    if "speaker" in config_dict["roles"]:
+        sio.enter_room(sid, f"{room_id}{ROOM_SPEAKERS_SUFFIX}")
+        if client_id not in room.speakers:
+            room.speakers.append(client_id)
+    else:
+        sio.leave_room(sid, f"{room_id}{ROOM_SPEAKERS_SUFFIX}")
+        # If the person is no longer a speaker they should no longer be able to lock the server
+        if server_lock and server_lock.get("client_id") == client_id:
+            logger.info(
+                f"🔓 Server is now unlocked from client {server_lock.get('client_id')} with name/info: {server_lock.get('name')}"
+            )
+            server_lock = None
+        if member.transcoder:
+            member.transcoder.close = True
+            member.transcoder = None
+        room.speakers = [
+            speaker_id for speaker_id in room.speakers if speaker_id != client_id
+        ]
+
+    # Only speakers should be able to lock the server
+    if config_dict.get("lockServerName") is not None and "speaker" in config_dict.get(
+        "roles", {}
+    ):
+        # If something goes wrong and the server gets stuck in a locked state the client can
+        # force the server to remove the lock by passing the special name ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME
+        if (
+            server_lock is not None
+            and config_dict.get("lockServerName")
+            == ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME
+            # If we are locking the server completely we don't want someone to be able to unlock it
+            and not os.environ.get("LOCK_SERVER_COMPLETELY", "0") == "1"
+        ):
+            server_lock = None
+            logger.info(
+                f"🔓 Server lock has been reset by {client_id} using the escape hatch name {ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME}"
+            )
+
+        # If the server is not locked, set a lock. If it's already locked to this client, update the lock object
+        if server_lock is None or server_lock.get("client_id") == client_id:
+            # TODO: Add some sort of timeout as a backstop in case someone leaves the browser tab open after locking the server
+            server_lock = {
+                "name": config_dict.get("lockServerName"),
+                "client_id": client_id,
+                "member_object": member,
+            }
+            logger.info(
+                f"🔒 Server is now locked to client {server_lock.get('client_id')} with name/info: {server_lock.get('name')}\nThis client will have priority over all others until they disconnect."
+            )
+        # If the server is already locked to someone else, don't allow this client to lock it
+        elif server_lock is not None and server_lock.get("client_id") != client_id:
+            logger.warn(
+                f"⚠️  Server is already locked to client {server_lock.get('client_id')}. Ignoring request to lock to client {client_id}."
+            )
+            # TODO: Maybe throw an error here?
+
+    # Save the room_id to the socketio client session
+    await set_session_data(
+        sid,
+        client_id=client_id,
+        room_id=room_id,
+        room_object=room,
+        member_object=member,
+        context_obj=context,
+        transcriber=transcriber,
+    )
+
+    await emit_room_state_update(room)
+    await emit_server_state_update()
+
+    return {"roomsJoined": sio.rooms(sid), "roomID": room_id}
+
+
+def check_and_lock_single_user(client_id, member):
+    global server_lock
+
+    if SINGLE_USER is None:
+        return True
+
+    if server_lock is None:
+        server_lock = {
+            "name": "single_user",
+            "client_id": client_id,
+            "member_object": member,
+        }
+        return True
+
+    return server_lock["client_id"] == client_id
+
+
+# @sio.on("disconnect")
+# @catch_and_log_exceptions_for_sio_event_handlers
+# async def disconnect(sid):
+#     logger.info(f"📤 [event: disconnected] sid={sid}")
+#     # Additional code to handle the disconnect event
+
+
+# TODO: Add code to prevent more than one speaker from connecting/streaming at a time
+@sio.event
+@catch_and_log_exceptions_for_sio_event_handlers
+async def configure_stream(sid, config):
+    session_data = await get_session_data(sid)
+    client_id, member, room, transcriber = itemgetter(
+        "client_id", "member_object", "room_object", "transcriber"
+    )(session_data)
+
+    logger.debug(
+        f"[event: configure_stream][{room}] Received stream config from {member}\n{pformat(config)}"
+    )
+
+    if member is None or room is None:
+        logger.error(
+            f"Received stream config from {member}, but member or room is None. This should not happen."
+        )
+        return {"status": "error", "message": "member_or_room_is_none"}
+
+    # if not allow_speaker(room, client_id):
+    #     logger.error(
+    #         f"In MAX_SPEAKERS mode we only allow one speaker at a time. Ignoring request to configure stream from client {client_id}."
+    #     )
+    #     return {"status": "error", "message": "max_speakers"}
+
+    # If there is a server lock WITH an active transcoder session, prevent other users from configuring and starting a stream
+    # If the server lock client does NOT have an active transcoder session allow this to proceed, knowing that
+    # this stream will be interrupted if the server lock client starts streaming
+    if (
+        server_lock is not None
+        and server_lock.get("client_id") != client_id
+        and server_lock.get("member_object")
+        and server_lock.get("member_object").transcoder is not None
+    ):
+        logger.warn(
+            f"Server is locked to client {server_lock.get('client_id')}. Ignoring request to configure stream from client {client_id}."
+        )
+        return {"status": "error", "message": "server_locked"}
+
+    debug = config.get("debug")
+    async_processing = config.get("async_processing")
+    manual_transcribe = config.get("manual_transcribe")
+    member.manual_transcribe = manual_transcribe
+
+    if manual_transcribe:
+        await transcriber.start()
+    else:
+        # Currently s2s, s2t or s2s&t
+        model_type = config.get("model_type")
+        member.requested_output_type = model_type
+
+        model_name = config.get("model_name")
+
+        try:
+            agent = available_agents.get_agent_or_throw(model_name)
+        except NoAvailableAgentException as e:
+            logger.warn(f"Error while getting agent: {e}")
+            # await sio.emit("error", str(e), to=sid)
+            await sio.disconnect(sid)
+            return {"status": "error", "message": str(e)}
+
+        if member.transcoder:
+            logger.warn(
+                "Member already has a transcoder configured. Closing it, and overwriting with a new transcoder..."
+            )
+            member.transcoder.close = True
+
+        t0 = time.time()
+        try:
+            member.transcoder = SimulevalTranscoder(
+                agent,
+                config["rate"],
+                debug=debug,
+                buffer_limit=int(config["buffer_limit"]),
+            )
+        except Exception as e:
+            logger.warn(f"Got exception while initializing agents: {e}")
+            # await sio.emit("error", str(e), to=sid)
+            await sio.disconnect(sid)
+            return {"status": "error", "message": str(e)}
+
+        t1 = time.time()
+        logger.debug(f"Booting up VAD and transcoder took {t1-t0} sec")
+
+        # TODO: if async_processing is false, then we need to run transcoder.process_pipeline_once() whenever we receive audio, or at some other sensible interval
+        if async_processing:
+            member.transcoder.start()
+
+    # We need to emit a room state update here since room state now includes # of active transcoders
+    await emit_room_state_update(room)
+    await emit_server_state_update()
+
+    return {"status": "ok", "message": "server_ready"}
+
+
+# The config here is a partial config, meaning it may not contain all the config values -- only the ones the user
+# wants to change
+@sio.on("set_dynamic_config")
+@catch_and_log_exceptions_for_sio_event_handlers
+async def set_dynamic_config(
+    sid,
+    # partial_config's type is defined in StreamingTypes.ts
+    partial_config,
+):
+    session_data = await get_session_data(sid)
+
+    member = None
+    context = None
+    if session_data:
+        member = session_data.get("member_object")
+        context = session_data.get("context_obj")
+
+    if member:
+        new_dynamic_config = {
+            **(member.transcoder_dynamic_config or {}),
+            **partial_config,
+        }
+        logger.info(
+            f"[set_dynamic_config] Setting new dynamic config:\n\n{pformat(new_dynamic_config)}\n"
+        )
+        member.transcoder_dynamic_config = new_dynamic_config
+
+    if context:
+        context.set_language(partial_config["targetLanguage"])
+
+    # TODO set transcriber language
+
+    return {"status": "ok", "message": "dynamic_config_set"}
+
+
+@sio.event
+@catch_and_log_exceptions_for_sio_event_handlers
+async def incoming_audio(sid, blob):
+    session_data = await get_session_data(sid)
+
+    client_id = None
+    member = None
+    room = None
+    context = None
+    transcriber = None
+
+    if session_data:
+        client_id = session_data.get("client_id")
+        member = session_data.get("member_object")
+        room = session_data.get("room_object")
+        context = session_data.get("context_obj")
+        transcriber = session_data.get("transcriber")
+
+    logger.debug(f"[event: incoming_audio] from member {member}")
+
+    # If the server is locked by someone else, kill our transcoder and ignore incoming audio
+    # If the server lock client does NOT have an active transcoder session allow this incoming audio pipeline to proceed,
+    # knowing that this stream will be interrupted if the server lock client starts streaming
+    if member.manual_transcribe:
+        print(blob)
+        await transcriber.sendAudio(blob)
+        return
+
+    if (
+        server_lock is not None
+        and server_lock.get("client_id") != client_id
+        and server_lock.get("member_object")
+        and server_lock.get("member_object").transcoder is not None
+    ):
+        # TODO: Send an event to the client to let them know their streaming session has been killed
+        if member.transcoder:
+            member.transcoder.close = True
+            member.transcoder = None
+            # Update both room state and server state given that the number of active transcoders has changed
+            if room:
+                await emit_room_state_update(room)
+            await emit_server_state_update()
+        logger.warn(
+            f"[incoming_audio] Server is locked to client {server_lock.get('client_id')}. Ignoring incoming audio from client {client_id}."
+        )
+        return
+
+    if member is None or room is None:
+        logger.error(
+            f"[incoming_audio] Received incoming_audio from {member}, but member or room is None. This should not happen."
+        )
+        return
+
+    if member.manual_transcribe:
+        transcriber.sendAudio(blob)
+    else:
+        # NOTE: bytes and bytearray are very similar, but bytes is immutable, and is what is returned by socketio
+        if not isinstance(blob, bytes):
+            logger.error(
+                f"[incoming_audio] Received audio from {member}, but it was not of type `bytes`. type(blob) = {type(blob)}"
+            )
+            return
+
+        if member.transcoder is None:
+            logger.error(
+                f"[incoming_audio] Received audio from {member}, but no transcoder configured to process it (member.transcoder is None). This should not happen."
+            )
+            return
+
+        member.transcoder.process_incoming_bytes(
+            blob, dynamic_config=member.transcoder_dynamic_config
+        )
+
+        # Send back any available model output
+        # NOTE: In theory it would make sense remove this from the incoming_audio handler and
+        # handle this in a dedicated thread that checks for output and sends it right away,
+        # but in practice for our limited demo use cases this approach didn't add noticeable
+        # latency, so we're keeping it simple for now.
+        events = get_transcoder_output_events(member.transcoder)
+        logger.debug(f"[incoming_audio] transcoder output events: {len(events)}")
+
+        if len(events) == 0:
+            logger.debug("[incoming_audio] No transcoder output to send")
+        else:
+            for e in events:
+                if e[
+                    "event"
+                ] == "translation_speech" and member.requested_output_type in [
+                    "s2s",
+                    "s2s&t",
+                ]:
+                    logger.debug("[incoming_audio] Sending translation_speech event")
+                    await sio.emit(
+                        "translation_speech", e, room=f"{room.room_id}_listeners"
+                    )
+                elif e[
+                    "event"
+                ] == "translation_text" and member.requested_output_type in [
+                    "s2t",
+                    "s2s&t",
+                ]:
+                    logger.debug("[incoming_audio] Sending translation_text event")
+                    await sio.emit(
+                        "translation_text", e, room=f"{room.room_id}_listeners"
+                    )
+                    context.add_text_chunk(e["payload"])
+                else:
+                    logger.error(
+                        f"[incoming_audio] Unexpected event type: {e['event']}"
+                    )
+
+    new_context = context.get_current_context()
+    if new_context:
+        await sio.emit(
+            "context",
+            {"event": "context", "payload": new_context},
+            room=f"{room.room_id}_listeners",
+        )
+    return
+
+
+@sio.event
+@catch_and_log_exceptions_for_sio_event_handlers
+async def stop_stream(sid):
+    session_data = await get_session_data(sid)
+    client_id, member, room = itemgetter("client_id", "member_object", "room_object")(
+        session_data
+    )
+
+    logger.debug(f"[event: stop_stream][{room}] Attempting to stop stream for {member}")
+
+    if member is None or room is None:
+        message = f"Received stop_stream from {member}, but member or room is None. This should not happen."
+        logger.error(message)
+        return {"status": "error", "message": message}
+
+    # In order to stop the stream and end the transcoder thread, set close to True and unset it for the member
+    if member.transcoder:
+        member.transcoder.close = True
+        member.transcoder = None
+    else:
+        message = f"Received stop_stream from {member}, but member.transcoder is None. This should not happen."
+        logger.warn(message)
+
+    # We need to emit a room state update here since room state now includes # of active transcoders
+    await emit_room_state_update(room)
+    # Emit a server state update now that we've changed the number of active transcoders
+    await emit_server_state_update()
+
+    return {"status": "ok", "message": "Stream stopped"}
+
+
+@sio.on("clear_transcript_for_all")
+@catch_and_log_exceptions_for_sio_event_handlers
+async def clear_transcript_for_all(sid):
+    session_data = await get_session_data(sid)
+
+    room = session_data.get("room_object")
+
+    if room:
+        await sio.emit("clear_transcript", room=f"{room.room_id}")
+    else:
+        logger.error("[clear_transcript] room is None. This should not happen.")
+
+
+@sio.event
+@catch_and_log_exceptions_for_sio_event_handlers
+async def set_name(sid, name):
+    logger.info(f"[Event: set_name] name={name}")
+    await sio.save_session(sid, {"name": name})
diff --git a/seamless-server/requirements.txt b/seamless-server/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0c8c61e16852e37dd3f49bd3df7cd3103847cd83
--- /dev/null
+++ b/seamless-server/requirements.txt
@@ -0,0 +1,34 @@
+# seamless_communication
+git+https://github.com/facebookresearch/seamless_communication.git
+# ./whl/seamless_communication-1.0.0-py3-none-any.whl
+Flask==2.1.3
+Flask_Sockets==0.2.1
+g2p_en==2.1.0
+gevent==22.10.2
+gevent_websocket==0.10.1
+librosa==0.9.2
+numpy==1.24.4
+openai_whisper==20230124
+protobuf==4.24.2
+psola==0.0.1
+pydub==0.25.1
+silero==0.4.1
+soundfile==0.11.0
+stable_ts==1.4.0
+# torch  # to be installed by user for desired PyTorch version
+# simuleval  # to be installed by seamless_communication
+Werkzeug==2.0.3
+whisper==1.1.10
+colorlog==6.7.0
+python-socketio==5.9.0
+uvicorn[standard]==0.23.2
+parallel-wavegan==0.5.5
+python-jose[cryptography]==3.3.0
+starlette==0.32.0.post1 
+hf_transfer==0.1.4
+huggingface_hub==0.19.
+google-auth
+python-dotenv
+deepgram-sdk
+sentencepiece
+fairseq2
\ No newline at end of file
diff --git a/seamless-server/run_docker.sh b/seamless-server/run_docker.sh
new file mode 100644
index 0000000000000000000000000000000000000000..617f0fb927fcd5b2814045fc4205053ca3b784b2
--- /dev/null
+++ b/seamless-server/run_docker.sh
@@ -0,0 +1,5 @@
+# !/bin/bash
+if [ -f models/Seamless/pretssel_melhifigan_wm.pt ] ; then
+    export USE_EXPRESSIVE_MODEL=1;
+fi
+uvicorn new:app --host 0.0.0.0 --port 7860 --reload
\ No newline at end of file
diff --git a/seamless-server/server.py b/seamless-server/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..81a7e0aeb9af1229a4d7166233415d96068945f8
--- /dev/null
+++ b/seamless-server/server.py
@@ -0,0 +1,288 @@
+from operator import itemgetter
+import os
+from urllib import parse
+from pprint import pformat
+import socketio
+import time
+import logging
+from starlette.applications import Starlette
+from starlette.routing import Mount, Route
+from starlette.staticfiles import StaticFiles
+from dotenv import load_dotenv
+
+load_dotenv()
+
+from src.auth import google_auth_check
+from src.client import Client
+from src.context import ContextManager
+from src.transcriber import Transcriber
+
+from src.simuleval_agent_directory import NoAvailableAgentException
+from src.simuleval_agent_directory import SimulevalAgentDirectory
+from src.simuleval_transcoder import SimulevalTranscoder
+from src.transcoder_helpers import get_transcoder_output_events
+from src.logging import (
+    initialize_logger,
+    catch_and_log_exceptions_for_sio_event_handlers,
+)
+
+logger = initialize_logger(__name__, level=logging.WARNING)
+print("=" * 20 + " ⭐️ Starting Server... ⭐️ " + "=" * 20)
+
+sio = socketio.AsyncServer(
+    async_mode="asgi",
+    cors_allowed_origins="*",
+    logger=logger,
+    # engineio_logger=logger,
+)
+socketio_app = socketio.ASGIApp(sio)
+
+app_routes = [
+    Mount("/ws", app=socketio_app),
+]
+app = Starlette(debug=True, routes=app_routes)
+
+# Specify specific models to load (some environments have issues loading multiple models)
+# See AgentWithInfo with JSON format details.
+models_override = os.environ.get("MODELS_OVERRIDE")
+
+available_agents = SimulevalAgentDirectory()
+logger.info("Building and adding agents...")
+if models_override is not None:
+    logger.info(f"MODELS_OVERRIDE supplied from env vars: {models_override}")
+available_agents.build_and_add_agents(models_override)
+
+agents_capabilities_for_json = available_agents.get_agents_capabilities_list_for_json()
+
+
+clients = {}
+
+
+@sio.on("connect")
+@catch_and_log_exceptions_for_sio_event_handlers(logger, sio)
+async def connect(sid, environ):
+    logger.info(f"📥 [event: connected] sid={sid}")
+
+    # TODO: Sanitize/validate query param input
+    query_params = dict(parse.parse_qsl(environ["QUERY_STRING"]))
+    client_id = query_params.get("clientID")
+    token = query_params.get("token")
+
+    if google_auth_check(token) is None:
+        await sio.emit("auth_error", "Not authenticated", to=sid)
+        logger.info("Invalid auth token, Disconnecting...")
+        await sio.disconnect(sid)
+        return
+
+    logger.debug(f"query_params:\n{pformat(query_params)}")
+
+    if client_id is None:
+        logger.info("No clientID provided. Disconnecting...")
+        await sio.disconnect(sid)
+        return
+
+    clients[sid] = Client(client_id)
+
+
+@sio.on("*")
+async def catch_all(event, sid, data):
+    logger.info(f"[unhandled event: {event}] sid={sid} data={data}")
+
+
+@sio.event
+@catch_and_log_exceptions_for_sio_event_handlers(logger, sio)
+async def configure_stream(sid, config):
+    client_obj = clients[sid]
+    logger.warning(sid)
+
+    if client_obj is None:
+        logger.error(f"No client object for {sid}")
+        await sio.disconnect(sid)
+        return {"status": "error", "message": "member_or_room_is_none"}
+
+    debug = config.get("debug")
+    async_processing = config.get("async_processing")
+    manual_transcribe = config.get("manual_transcribe")
+    client_obj.manual_transcribe = manual_transcribe
+
+    if manual_transcribe:
+        client_obj.transcriber = Transcriber()
+        client_obj.transcriber.start()
+    else:
+        # Currently s2s, s2t or s2s&t
+        model_type = config.get("model_type")
+        client_obj.requested_output_type = model_type
+
+        model_name = config.get("model_name")
+
+        try:
+            agent = available_agents.get_agent_or_throw(model_name)
+        except NoAvailableAgentException as e:
+            logger.warn(f"Error while getting agent: {e}")
+            await sio.disconnect(sid)
+            return {"status": "error", "message": str(e)}
+
+        if client_obj.transcoder:
+            logger.warn(
+                "Member already has a transcoder configured. Closing it, and overwriting with a new transcoder..."
+            )
+            client_obj.transcoder.close = True
+
+        t0 = time.time()
+        try:
+            client_obj.transcoder = SimulevalTranscoder(
+                agent,
+                config["rate"],
+                debug=debug,
+                buffer_limit=int(config["buffer_limit"]),
+            )
+        except Exception as e:
+            logger.warn(f"Got exception while initializing agents: {e}")
+            await sio.disconnect(sid)
+            return {"status": "error", "message": str(e)}
+
+        t1 = time.time()
+        logger.debug(f"Booting up VAD and transcoder took {t1-t0} sec")
+
+        # TODO: if async_processing is false, then we need to run transcoder.process_pipeline_once() whenever we receive audio, or at some other sensible interval
+        if async_processing:
+            client_obj.transcoder.start()
+
+    client_obj.context = ContextManager()
+    return {"status": "ok", "message": "server_ready"}
+
+
+@sio.on("set_dynamic_config")
+@catch_and_log_exceptions_for_sio_event_handlers(logger, sio)
+async def set_dynamic_config(
+    sid,
+    partial_config,
+):
+    client_obj = clients[sid]
+
+    if client_obj is None:
+        logger.error(f"No client object for {sid}")
+        await sio.disconnect(sid)
+        return {"status": "error", "message": "member_or_room_is_none"}
+
+    new_dynamic_config = {
+        **(client_obj.transcoder_dynamic_config or {}),
+        **partial_config,
+    }
+    logger.info(
+        f"[set_dynamic_config] Setting new dynamic config:\n\n{pformat(new_dynamic_config)}\n"
+    )
+
+    client_obj.transcoder_dynamic_config = new_dynamic_config
+
+    if client_obj.context:
+        client_obj.context.set_language(partial_config["targetLanguage"])
+
+    # TODO set transcriber language
+
+    return {"status": "ok", "message": "dynamic_config_set"}
+
+
+@sio.event
+async def incoming_audio(sid, blob):
+    client_obj = clients[sid]
+
+    if client_obj is None:
+        logger.error(f"No client object for {sid}")
+        await sio.disconnect(sid)
+        return {"status": "error", "message": "member_or_room_is_none"}
+
+    if client_obj.manual_transcribe:
+        client_obj.transcriber.send_audio(blob)
+    else:
+        # NOTE: bytes and bytearray are very similar, but bytes is immutable, and is what is returned by socketio
+        if not isinstance(blob, bytes):
+            logger.error(
+                f"[incoming_audio] Received audio from {sid}, but it was not of type `bytes`. type(blob) = {type(blob)}"
+            )
+            return
+
+        if client_obj.transcoder is None:
+            logger.error(
+                f"[incoming_audio] Received audio from {sid}, but no transcoder configured to process it (member.transcoder is None). This should not happen."
+            )
+            return
+
+        client_obj.transcoder.process_incoming_bytes(
+            blob, dynamic_config=client_obj.transcoder_dynamic_config
+        )
+
+        # Send back any available model output
+        # NOTE: In theory it would make sense remove this from the incoming_audio handler and
+        # handle this in a dedicated thread that checks for output and sends it right away,
+        # but in practice for our limited demo use cases this approach didn't add noticeable
+        # latency, so we're keeping it simple for now.
+        events = get_transcoder_output_events(client_obj.transcoder)
+        logger.debug(f"[incoming_audio] transcoder output events: {len(events)}")
+
+        if len(events) == 0:
+            logger.debug("[incoming_audio] No transcoder output to send")
+        else:
+            for e in events:
+                if e[
+                    "event"
+                ] == "translation_speech" and client_obj.requested_output_type in [
+                    "s2s",
+                    "s2s&t",
+                ]:
+                    logger.debug("[incoming_audio] Sending translation_speech event")
+                    await sio.emit("translation_speech", e, room=sid)
+                elif e[
+                    "event"
+                ] == "translation_text" and client_obj.requested_output_type in [
+                    "s2t",
+                    "s2s&t",
+                ]:
+                    logger.debug("[incoming_audio] Sending translation_text event")
+                    await sio.emit("translation_text", e, room=sid)
+                    client_obj.context.add_text_chunk(e["payload"])
+                else:
+                    logger.error(
+                        f"[incoming_audio] Unexpected event type: {e['event']}"
+                    )
+    new_context = client_obj.context.get_current_context()
+    if new_context:
+        await sio.emit(
+            "context",
+            {"event": "context", "payload": new_context},
+            room=sid,
+        )
+    return
+
+
+@sio.event
+async def stop_stream(sid):
+    client_obj = clients[sid]
+
+    if client_obj is None:
+        logger.error(f"No client object for {sid}")
+        await sio.disconnect(sid)
+        return {"status": "error", "message": "member_or_room_is_none"}
+
+    if client_obj.transcoder:
+        client_obj.transcoder.close = True
+        client_obj.transcoder = None
+
+    if client_obj.transcriber:
+        client_obj.transcriber.close_connection()
+
+
+@sio.event
+async def disconnect(sid):
+    client_obj = clients[sid]
+    if client_obj is None:
+        return
+
+    if client_obj.transcriber:
+        client_obj.transcriber.stop()
+
+    if client_obj.transcoder:
+        client_obj.transcoder.close = True
+        client_obj.transcoder = None
+
+    del clients[sid]
diff --git a/seamless-server/src/auth.py b/seamless-server/src/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..18b1da3b87c9fe4bf6ca93fa943fab26e6b50310
--- /dev/null
+++ b/seamless-server/src/auth.py
@@ -0,0 +1,20 @@
+from src.logging import initialize_logger
+import requests
+
+logger = initialize_logger(__name__)
+
+
+def google_auth_check(token):
+    try:
+        response = requests.get(
+            "https://www.googleapis.com/oauth2/v3/tokeninfo",
+            params={"access_token": token},
+        )
+        if response.status_code == 200:
+            token_info = response.json()
+            return token_info
+        else:
+            return None
+    except Exception as e:
+        logger.info(e)
+        return None
diff --git a/seamless-server/src/client.py b/seamless-server/src/client.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc7122c4e1079f99d50f34098f3fffe71cc1b06e
--- /dev/null
+++ b/seamless-server/src/client.py
@@ -0,0 +1,23 @@
+class Client:
+    def __init__(
+        self,
+        client_id,
+    ) -> None:
+        self.client_id = client_id
+        self.connection_status = "connected"
+        self.transcoder = None
+        self.transcriber = None
+        self.context = None
+        self.requested_output_type = None
+        self.transcoder_dynamic_config = None
+        self.manual_transcribe = None
+
+    def __str__(self) -> str:
+        return f"{self.name} (id: {self.client_id[:4]}...) ({self.connection_status})"
+
+    def to_json(self):
+        self_vars = vars(self)
+        return {
+            **self_vars,
+            "transcoder": self.transcoder is not None,
+        }
diff --git a/seamless-server/src/context.py b/seamless-server/src/context.py
new file mode 100644
index 0000000000000000000000000000000000000000..e18e5bd0a5d14f36b066a453d29e7a2ad0bb3c43
--- /dev/null
+++ b/seamless-server/src/context.py
@@ -0,0 +1,83 @@
+import requests
+import json
+from threading import Thread
+from src.logging import initialize_logger
+import os
+
+# TODO get language key
+prompt = """
+Transcription: "[TRANSCRIPT]"
+Task: Give a concise, 1-sentence summary of what the speaker is talking about. 
+IMPORTANT: The summary must be in the language: [LANGUAGE].
+Return the response in JSON format with the following attribute: summary
+Response in JSON Format:
+"""
+
+
+logger = initialize_logger(__name__)
+
+
+class ContextManager:
+    def __init__(self):
+        self.text_buffer = ""
+        self.amt = 0
+        self.max_char_memory = 300
+        self.char_between_release = 200
+        self.language = None
+        self.current_context = {}
+
+    def get_current_context(self):
+        if self.current_context and self.current_context["read"] is False:
+            self.current_context["read"] = True
+            return self.current_context["text"]
+        return None
+
+    def summarize(self, text):
+        if self.language is None:
+            return
+        try:
+            url = "https://voice-llm.openai.azure.com/openai/deployments/voice-LLM/chat/completions?api-version=2023-12-01-preview"
+            headers = {
+                "Content-Type": "application/json",
+                "api-key": os.getenv("AZURE_API_KEY"),
+            }
+
+            body = {
+                "model": "gpt-35-turbo",
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": prompt.replace("[TRANSCRIPT]", text).replace(
+                            "[LANGUAGE]", self.language
+                        ),
+                    }
+                ],
+            }
+
+            response = requests.post(url, headers=headers, json=body)
+            response_data = response.json()
+            parsed = json.loads(response_data["choices"][0]["message"]["content"])[
+                "summary"
+            ]
+            self.current_context = {"text": parsed, "read": False}
+        except Exception as e:
+            logger.warning(e)
+
+    def add_text_chunk(self, text):
+        self.text_buffer += " " + text
+        cur_len = len(self.text_buffer)
+
+        # continously trim context to save memory
+        if len(self.text_buffer) > self.max_char_memory:
+            self.text_buffer = self.text_buffer[cur_len - self.max_char_memory :]
+
+        self.amt += len(text)
+        if self.amt > self.char_between_release:
+            self.amt = 0
+            thread = Thread(target=self.summarize, args=(self.text_buffer,))
+            thread.start()
+
+    def set_language(self, lang):
+        self.language = lang
+        self.text_buffer = ""
+        self.amt = 0
diff --git a/seamless-server/src/logging.py b/seamless-server/src/logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..db70ad7092d47f010061ca620dc0acafcbb64538
--- /dev/null
+++ b/seamless-server/src/logging.py
@@ -0,0 +1,58 @@
+import logging
+import colorlog
+import sys
+import time
+
+
+def initialize_logger(name, level=logging.WARNING):
+    logger = logging.getLogger(name)
+    logger.propagate = False
+    handler = colorlog.StreamHandler(stream=sys.stdout)
+    formatter = colorlog.ColoredFormatter(
+        "%(log_color)s[%(asctime)s][%(levelname)s][%(module)s]:%(reset)s %(message)s",
+        reset=True,
+        log_colors={
+            "DEBUG": "cyan",
+            "INFO": "green",
+            "WARNING": "yellow",
+            "ERROR": "red",
+            "CRITICAL": "red,bg_white",
+        },
+    )
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+    logger.setLevel(level)
+    return logger
+
+
+def catch_and_log_exceptions_for_sio_event_handlers(sio, logger):
+    # wrapper should have the same signature as the original function
+    def decorator(func):
+        async def catch_exception_wrapper(*args, **kwargs):
+            try:
+                return await func(*args, **kwargs)
+            except Exception as e:
+                message = f"[app_pubsub] Caught exception in '{func.__name__}' event handler:\n\n{e}"
+                logger.exception(message, stack_info=True)
+
+                try:
+                    exception_data = {
+                        "message": message,
+                        "timeEpochMs": int(time.time() * 1000),
+                    }
+
+                    # For now let's emit this to all clients. We ultimatley may want to emit it just to the room it's happening in.
+                    await sio.emit("server_exception", exception_data)
+                except Exception as inner_e:
+                    logger.exception(
+                        f"[app_pubsub] Caught exception while trying to emit server_exception event:\n{inner_e}"
+                    )
+
+                # Re-raise the exception so it's handled normally by the server
+                raise e
+
+        # Set the name of the wrapper to the name of the original function so that the socketio server can associate it with the right event
+        catch_exception_wrapper.__name__ = func.__name__
+        return catch_exception_wrapper
+
+    return decorator
diff --git a/seamless-server/src/room.py b/seamless-server/src/room.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee016df87410f62f65faa60d84e0c6c9cef8e0ae
--- /dev/null
+++ b/seamless-server/src/room.py
@@ -0,0 +1,65 @@
+# import json
+import uuid
+
+
+class Room:
+    def __init__(self, room_id) -> None:
+        self.room_id = room_id
+        # members is a dict from client_id to Member
+        self.members = {}
+
+        # listeners and speakers are lists of client_id's
+        self.listeners = []
+        self.speakers = []
+
+    def __str__(self) -> str:
+        return f"Room {self.room_id} ({len(self.members)} member{'s' if len(self.members) == 1 else ''})"
+
+    def to_json(self):
+        varsResult = vars(self)
+        # Remember: result is just a shallow copy, so result.members === self.members
+        # Because of that, we need to jsonify self.members without writing over result.members,
+        # which we do here via dictionary unpacking (the ** operator)
+        result = {
+            **varsResult,
+            "members": {key: value.to_json() for (key, value) in self.members.items()},
+            "activeTranscoders": self.get_active_transcoders(),
+        }
+
+        return result
+
+    def get_active_connections(self):
+        return len(
+            [m for m in self.members.values() if m.connection_status == "connected"]
+        )
+
+    def get_active_transcoders(self):
+        return len([m for m in self.members.values() if m.transcoder is not None])
+
+    def get_room_status_dict(self):
+        return {
+            "activeConnections": self.get_active_connections(),
+            "activeTranscoders": self.get_active_transcoders(),
+        }
+
+
+class Member:
+    def __init__(self, client_id, session_id, name) -> None:
+        self.client_id = client_id
+        self.session_id = session_id
+        self.name = name
+        self.connection_status = "connected"
+        self.transcoder = None
+        self.requested_output_type = None
+        self.transcoder_dynamic_config = None
+        self.manual_transcribe = None
+
+    def __str__(self) -> str:
+        return f"{self.name} (id: {self.client_id[:4]}...) ({self.connection_status})"
+
+    def to_json(self):
+        self_vars = vars(self)
+        return {
+            **self_vars,
+            "transcoder": self.transcoder is not None,
+        }
diff --git a/seamless-server/src/simuleval_agent_directory.py b/seamless-server/src/simuleval_agent_directory.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f0fc9022801d52c05f4f8823e4fb26e610e411a
--- /dev/null
+++ b/seamless-server/src/simuleval_agent_directory.py
@@ -0,0 +1,171 @@
+# Creates a directory in which to look up available agents
+
+import os
+from typing import List, Optional
+from src.simuleval_transcoder import SimulevalTranscoder
+import json
+import logging
+
+logger = logging.getLogger("socketio_server_pubsub")
+
+# fmt: off
+M4T_P0_LANGS = [
+    "eng",
+    "arb", "ben", "cat", "ces", "cmn", "cym", "dan",
+    "deu", "est", "fin", "fra", "hin", "ind", "ita",
+    "jpn", "kor", "mlt", "nld", "pes", "pol", "por",
+    "ron", "rus", "slk", "spa", "swe", "swh", "tel",
+    "tgl", "tha", "tur", "ukr", "urd", "uzn", "vie",
+]
+# fmt: on
+
+
+class NoAvailableAgentException(Exception):
+    pass
+
+
+class AgentWithInfo:
+    def __init__(
+        self,
+        agent,
+        name: str,
+        modalities: List[str],
+        target_langs: List[str],
+        # Supported dynamic params are defined in StreamingTypes.ts
+        dynamic_params: List[str] = [],
+        description="",
+        has_expressive: Optional[bool] = None,
+    ):
+        self.agent = agent
+        self.has_expressive = has_expressive
+        self.name = name
+        self.description = description
+        self.modalities = modalities
+        self.target_langs = target_langs
+        self.dynamic_params = dynamic_params
+
+    def get_capabilities_for_json(self):
+        return {
+            "name": self.name,
+            "description": self.description,
+            "modalities": self.modalities,
+            "targetLangs": self.target_langs,
+            "dynamicParams": self.dynamic_params,
+        }
+
+    @classmethod
+    def load_from_json(cls, config: str):
+        """
+        Takes in JSON array of models to load in, e.g.
+        [{"name": "s2s_m4t_emma-unity2_multidomain_v0.1", "description": "M4T model that supports simultaneous S2S and S2T", "modalities": ["s2t", "s2s"], "targetLangs": ["en"]},
+        {"name": "s2s_m4t_expr-emma_v0.1", "description": "ES-EN expressive model that supports S2S and S2T", "modalities": ["s2t", "s2s"], "targetLangs": ["en"]}]
+        """
+        configs = json.loads(config)
+        agents = []
+        for config in configs:
+            agent = SimulevalTranscoder.build_agent(config["name"])
+            agents.append(
+                AgentWithInfo(
+                    agent=agent,
+                    name=config["name"],
+                    modalities=config["modalities"],
+                    target_langs=config["targetLangs"],
+                )
+            )
+        return agents
+
+
+class SimulevalAgentDirectory:
+    # Available models. These are the directories where the models can be found, and also serve as an ID for the model.
+    seamless_streaming_agent = "SeamlessStreaming"
+    seamless_agent = "Seamless"
+
+    def __init__(self):
+        self.agents = []
+        self.did_build_and_add_agents = False
+
+    def add_agent(self, agent: AgentWithInfo):
+        self.agents.append(agent)
+
+    def build_agent_if_available(self, model_id, config_name=None):
+        agent = None
+        try:
+            if config_name is not None:
+                agent = SimulevalTranscoder.build_agent(
+                    model_id,
+                    config_name=config_name,
+                )
+            else:
+                agent = SimulevalTranscoder.build_agent(
+                    model_id,
+                )
+        except Exception as e:
+            from fairseq2.assets.error import AssetError
+            logger.warning("Failed to build agent %s: %s" % (model_id, e))
+            if isinstance(e, AssetError):
+                logger.warning(
+                    "Please download gated assets and set `gated_model_dir` in the config"
+                )
+            raise e
+
+        return agent
+
+    def build_and_add_agents(self, models_override=None):
+        if self.did_build_and_add_agents:
+            return
+
+        if models_override is not None:
+            agent_infos = AgentWithInfo.load_from_json(models_override)
+            for agent_info in agent_infos:
+                self.add_agent(agent_info)
+        else:
+            s2s_agent = None
+            if os.environ.get("USE_EXPRESSIVE_MODEL", "0") == "1":
+                logger.info("Building expressive model...")
+                s2s_agent = self.build_agent_if_available(
+                    SimulevalAgentDirectory.seamless_agent,
+                    config_name="vad_s2st_sc_24khz_main.yaml",
+                )
+                has_expressive = True
+            else:
+                logger.info("Building non-expressive model...")
+                s2s_agent = self.build_agent_if_available(
+                    SimulevalAgentDirectory.seamless_streaming_agent,
+                    config_name="vad_s2st_sc_main.yaml",
+                )
+                has_expressive = False
+
+            if s2s_agent:
+                self.add_agent(
+                    AgentWithInfo(
+                        agent=s2s_agent,
+                        name=SimulevalAgentDirectory.seamless_streaming_agent,
+                        modalities=["s2t", "s2s"],
+                        target_langs=M4T_P0_LANGS,
+                        dynamic_params=["expressive"],
+                        description="multilingual expressive model that supports S2S and S2T",
+                        has_expressive=has_expressive,
+                    )
+                )
+
+        if len(self.agents) == 0:
+            logger.error(
+                "No agents were loaded. This likely means you are missing the actual model files specified in simuleval_agent_directory."
+            )
+
+        self.did_build_and_add_agents = True
+
+    def get_agent(self, name):
+        for agent in self.agents:
+            if agent.name == name:
+                return agent
+        return None
+
+    def get_agent_or_throw(self, name):
+        agent = self.get_agent(name)
+        if agent is None:
+            raise NoAvailableAgentException("No agent found with name= %s" % (name))
+        return agent
+
+    def get_agents_capabilities_list_for_json(self):
+        return [agent.get_capabilities_for_json() for agent in self.agents]
diff --git a/seamless-server/src/simuleval_transcoder.py b/seamless-server/src/simuleval_transcoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e0ab09119d717f899a95c27c2335eaa51f64a1c
--- /dev/null
+++ b/seamless-server/src/simuleval_transcoder.py
@@ -0,0 +1,409 @@
+from simuleval.utils.agent import build_system_from_dir
+from typing import Any, List, Optional, Tuple, Union
+import numpy as np
+import soundfile
+import io
+import asyncio
+from simuleval.agents.pipeline import TreeAgentPipeline
+from simuleval.agents.states import AgentStates
+from simuleval.data.segments import Segment, EmptySegment, SpeechSegment
+import threading
+from pathlib import Path
+import time
+from g2p_en import G2p
+import torch
+import traceback
+import time
+import random
+from src.logging import initialize_logger
+from .speech_and_text_output import SpeechAndTextOutput
+
+MODEL_SAMPLE_RATE = 16_000
+
+logger = initialize_logger(__name__)
+
+
+class OutputSegments:
+    def __init__(self, segments: Union[List[Segment], Segment]):
+        if isinstance(segments, Segment):
+            segments = [segments]
+        self.segments: List[Segment] = [s for s in segments]
+
+    @property
+    def is_empty(self):
+        return all(segment.is_empty for segment in self.segments)
+
+    @property
+    def finished(self):
+        return all(segment.finished for segment in self.segments)
+
+    def compute_length(self, g2p):
+        lengths = []
+        for segment in self.segments:
+            if segment.data_type == "text":
+                lengths.append(len([x for x in g2p(segment.content) if x != " "]))
+            elif segment.data_type == "speech":
+                lengths.append(len(segment.content) / MODEL_SAMPLE_RATE)
+            elif isinstance(segment, EmptySegment):
+                continue
+            else:
+                logger.warning(
+                    f"Unexpected data_type: {segment.data_type} not in 'speech', 'text'"
+                )
+        return max(lengths)
+
+    @classmethod
+    def join_output_buffer(
+        cls, buffer: List[List[Segment]], output: SpeechAndTextOutput
+    ):
+        num_segments = len(buffer[0])
+        for i in range(num_segments):
+            segment_list = [
+                buffer[j][i]
+                for j in range(len(buffer))
+                if buffer[j][i].data_type is not None
+            ]
+            if len(segment_list) == 0:
+                continue
+            if len(set(segment.data_type for segment in segment_list)) != 1:
+                logger.warning(
+                    f"Data type mismatch at {i}: {set(segment.data_type for segment in segment_list)}"
+                )
+                continue
+            data_type = segment_list[0].data_type
+            if data_type == "text":
+                if output.text is not None:
+                    logger.warning("Multiple text outputs, overwriting!")
+                output.text = " ".join([segment.content for segment in segment_list])
+            elif data_type == "speech":
+                if output.speech_samples is not None:
+                    logger.warning("Multiple speech outputs, overwriting!")
+                speech_out = []
+                for segment in segment_list:
+                    speech_out += segment.content
+                output.speech_samples = speech_out
+                output.speech_sample_rate = segment.sample_rate
+            elif isinstance(segment_list[0], EmptySegment):
+                continue
+            else:
+                logger.warning(
+                    f"Invalid output buffer data type: {data_type}, expected 'speech' or 'text"
+                )
+
+        return output
+
+    def __repr__(self) -> str:
+        repr_str = str(self.segments)
+        return f"{self.__class__.__name__}(\n\t{repr_str}\n)"
+
+
+class SimulevalTranscoder:
+    def __init__(self, agent, sample_rate, debug, buffer_limit):
+        self.agent = agent.agent
+        self.has_expressive = agent.has_expressive
+        self.input_queue = asyncio.Queue()
+        self.output_queue = asyncio.Queue()
+        self.states = self.agent.build_states()
+        if debug:
+            self.get_states_root().debug = True
+        self.incoming_sample_rate = sample_rate
+        self.close = False
+        self.g2p = G2p()
+
+        # buffer all outgoing translations within this amount of time
+        self.output_buffer_idle_ms = 5000
+        self.output_buffer_size_limit = (
+            buffer_limit  # phonemes for text, seconds for speech
+        )
+        self.output_buffer_cur_size = 0
+        self.output_buffer: List[List[Segment]] = []
+        self.speech_output_sample_rate = None
+
+        self.last_output_ts = time.time() * 1000
+        self.timeout_ms = (
+            30000  # close the transcoder thread after this amount of silence
+        )
+        self.first_input_ts = None
+        self.first_output_ts = None
+        self.debug = debug
+        self.debug_ts = f"{time.time()}_{random.randint(1000, 9999)}"
+        if self.debug:
+            debug_folder = Path(__file__).resolve().parent.parent / "debug"
+            self.test_incoming_wav = soundfile.SoundFile(
+                debug_folder / f"{self.debug_ts}_test_incoming.wav",
+                mode="w+",
+                format="WAV",
+                subtype="PCM_16",
+                samplerate=self.incoming_sample_rate,
+                channels=1,
+            )
+            self.get_states_root().test_input_segments_wav = soundfile.SoundFile(
+                debug_folder / f"{self.debug_ts}_test_input_segments.wav",
+                mode="w+",
+                format="WAV",
+                samplerate=MODEL_SAMPLE_RATE,
+                channels=1,
+            )
+
+    def get_states_root(self) -> AgentStates:
+        if isinstance(self.agent, TreeAgentPipeline):
+            # self.states is a dict
+            return self.states[self.agent.source_module]
+        else:
+            # self.states is a list
+            return self.states[0]
+
+    def reset_states(self):
+        if isinstance(self.agent, TreeAgentPipeline):
+            states_iter = self.states.values()
+        else:
+            states_iter = self.states
+        for state in states_iter:
+            state.reset()
+
+    def debug_log(self, *args):
+        if self.debug:
+            logger.info(*args)
+
+    @classmethod
+    def build_agent(cls, model_path, config_name):
+        logger.info(f"Building simuleval agent: {model_path}, {config_name}")
+        agent = build_system_from_dir(
+            Path(__file__).resolve().parent.parent / f"models/{model_path}",
+            config_name=config_name,
+        )
+        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+        logger.warning(f"agent built on {device}")
+        agent.to(device, fp16=True)
+        logger.info(
+            f"Successfully built simuleval agent {model_path} on device {device}"
+        )
+
+        return agent
+
+    def process_incoming_bytes(self, incoming_bytes, dynamic_config):
+        # TODO: We probably want to do some validation on dynamic_config to ensure it has what we needs
+        segment, sr = self._preprocess_wav(incoming_bytes)
+        segment = SpeechSegment(
+            content=segment,
+            sample_rate=sr,
+            tgt_lang=dynamic_config.get("targetLanguage"),
+            config=dynamic_config,
+        )
+        if dynamic_config.get("expressive") is True and self.has_expressive is False:
+            logger.warning(
+                "Passing 'expressive' but the agent does not support expressive output!"
+            )
+        # # segment is array([0, 0, 0, ..., 0, 0, 0], dtype=int16)
+        self.input_queue.put_nowait(segment)
+
+    def get_input_segment(self):
+        if self.input_queue.empty():
+            return None
+        chunk = self.input_queue.get_nowait()
+        self.input_queue.task_done()
+        return chunk
+
+    def convert_waveform(
+        self,
+        waveform: Union[np.ndarray, torch.Tensor],
+        sample_rate: int,
+        normalize_volume: bool = False,
+        to_mono: bool = False,
+        to_sample_rate: Optional[int] = None,
+    ) -> Tuple[Union[np.ndarray, torch.Tensor], int]:
+        """convert a waveform:
+        - to a target sample rate
+        - from multi-channel to mono channel
+        - volume normalization
+
+        Args:
+            waveform (numpy.ndarray or torch.Tensor): 2D original waveform
+                (channels x length)
+            sample_rate (int): original sample rate
+            normalize_volume (bool): perform volume normalization
+            to_mono (bool): convert to mono channel if having multiple channels
+            to_sample_rate (Optional[int]): target sample rate
+        Returns:
+            waveform (numpy.ndarray): converted 2D waveform (channels x length)
+            sample_rate (float): target sample rate
+        """
+        try:
+            import torchaudio.sox_effects as ta_sox
+        except ImportError:
+            raise ImportError("Please install torchaudio: pip install torchaudio")
+
+        effects = []
+        if normalize_volume:
+            effects.append(["gain", "-n"])
+        if to_sample_rate is not None and to_sample_rate != sample_rate:
+            effects.append(["rate", f"{to_sample_rate}"])
+        if to_mono and waveform.shape[0] > 1:
+            effects.append(["channels", "1"])
+        if len(effects) > 0:
+            is_np_input = isinstance(waveform, np.ndarray)
+            _waveform = torch.from_numpy(waveform) if is_np_input else waveform
+            converted, converted_sample_rate = ta_sox.apply_effects_tensor(
+                _waveform, sample_rate, effects
+            )
+            if is_np_input:
+                converted = converted.numpy()
+            return converted, converted_sample_rate
+        return waveform, sample_rate
+
+    def _preprocess_wav(self, data: Any) -> Tuple[np.ndarray, int]:
+        segment, sample_rate = soundfile.read(
+            io.BytesIO(data),
+            dtype="float32",
+            always_2d=True,
+            frames=-1,
+            start=0,
+            format="RAW",
+            subtype="PCM_16",
+            samplerate=self.incoming_sample_rate,
+            channels=1,
+        )
+        if self.debug:
+            self.test_incoming_wav.seek(0, soundfile.SEEK_END)
+            self.test_incoming_wav.write(segment)
+
+        segment = segment.T
+        segment, new_sample_rate = self.convert_waveform(
+            segment,
+            sample_rate,
+            normalize_volume=False,
+            to_mono=True,
+            to_sample_rate=MODEL_SAMPLE_RATE,
+        )
+
+        assert MODEL_SAMPLE_RATE == new_sample_rate
+        segment = segment.squeeze(axis=0)
+        return segment, new_sample_rate
+
+    def process_pipeline_impl(self, input_segment):
+        try:
+            with torch.no_grad():
+                output_segment = OutputSegments(
+                    self.agent.pushpop(input_segment, self.states)
+                )
+            if (
+                self.get_states_root().first_input_ts is not None
+                and self.first_input_ts is None
+            ):
+                # TODO: this is hacky
+                self.first_input_ts = self.get_states_root().first_input_ts
+
+            if not output_segment.is_empty:
+                self.output_queue.put_nowait(output_segment)
+
+            if output_segment.finished:
+                self.debug_log("OUTPUT SEGMENT IS FINISHED. Resetting states.")
+
+                self.reset_states()
+
+                if self.debug:
+                    # when we rebuild states, this value is reset to whatever
+                    # is in the system dir config, which defaults debug=False.
+                    self.get_states_root().debug = True
+        except Exception as e:
+            logger.error(f"Got exception while processing pipeline: {e}")
+            traceback.print_exc()
+        return input_segment
+
+    def process_pipeline_loop(self):
+        if self.close:
+            return  # closes the thread
+
+        self.debug_log("processing_pipeline")
+        while not self.close:
+            input_segment = self.get_input_segment()
+            if input_segment is None:
+                if self.get_states_root().is_fresh_state:  # TODO: this is hacky
+                    time.sleep(0.3)
+                else:
+                    time.sleep(0.03)
+                continue
+            self.process_pipeline_impl(input_segment)
+        self.debug_log("finished processing_pipeline")
+
+    def process_pipeline_once(self):
+        if self.close:
+            return
+
+        self.debug_log("processing pipeline once")
+        input_segment = self.get_input_segment()
+        if input_segment is None:
+            return
+        self.process_pipeline_impl(input_segment)
+        self.debug_log("finished processing_pipeline_once")
+
+    def get_output_segment(self):
+        if self.output_queue.empty():
+            return None
+
+        output_chunk = self.output_queue.get_nowait()
+        self.output_queue.task_done()
+        return output_chunk
+
+    def start(self):
+        self.debug_log("starting transcoder in a thread")
+        threading.Thread(target=self.process_pipeline_loop).start()
+
+    def first_translation_time(self):
+        return round((self.first_output_ts - self.first_input_ts) / 1000, 2)
+
+    def get_buffered_output(self) -> SpeechAndTextOutput:
+        now = time.time() * 1000
+        self.debug_log(f"get_buffered_output queue size: {self.output_queue.qsize()}")
+        while not self.output_queue.empty():
+            tmp_out = self.get_output_segment()
+            if tmp_out and tmp_out.compute_length(self.g2p) > 0:
+                if len(self.output_buffer) == 0:
+                    self.last_output_ts = now
+                self._populate_output_buffer(tmp_out)
+                self._increment_output_buffer_size(tmp_out)
+
+                if tmp_out.finished:
+                    self.debug_log("tmp_out.finished")
+                    res = self._gather_output_buffer_data(final=True)
+                    self.debug_log(f"gathered output data: {res}")
+                    self.output_buffer = []
+                    self.increment_output_buffer_size = 0
+                    self.last_output_ts = now
+                    self.first_output_ts = now
+                    return res
+            else:
+                self.debug_log("tmp_out.compute_length is not > 0")
+
+        if len(self.output_buffer) > 0 and (
+            now - self.last_output_ts >= self.output_buffer_idle_ms
+            or self.output_buffer_cur_size >= self.output_buffer_size_limit
+        ):
+            self.debug_log(
+                "[get_buffered_output] output_buffer is not empty. getting res to return."
+            )
+            self.last_output_ts = now
+            res = self._gather_output_buffer_data(final=False)
+            self.debug_log(f"gathered output data: {res}")
+            self.output_buffer = []
+            self.output_buffer_phoneme_count = 0
+            self.first_output_ts = now
+            return res
+        else:
+            self.debug_log("[get_buffered_output] output_buffer is empty...")
+            return None
+
+    def _gather_output_buffer_data(self, final):
+        output = SpeechAndTextOutput()
+        output.final = final
+        output = OutputSegments.join_output_buffer(self.output_buffer, output)
+        return output
+
+    def _increment_output_buffer_size(self, segment: OutputSegments):
+        self.output_buffer_cur_size += segment.compute_length(self.g2p)
+
+    def _populate_output_buffer(self, segment: OutputSegments):
+        self.output_buffer.append(segment.segments)
+
+    def _compute_phoneme_count(self, string: str) -> int:
+        return len([x for x in self.g2p(string) if x != " "])
diff --git a/seamless-server/src/speech_and_text_output.py b/seamless-server/src/speech_and_text_output.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a4e5c7884f8256df01b9edca6d5b1759d5ab5ba
--- /dev/null
+++ b/seamless-server/src/speech_and_text_output.py
@@ -0,0 +1,15 @@
+# Provides a container to return both speech and text output from our model at the same time
+
+
+class SpeechAndTextOutput:
+    def __init__(
+        self,
+        text: str = None,
+        speech_samples: list = None,
+        speech_sample_rate: float = None,
+        final: bool = False,
+    ):
+        self.text = text
+        self.speech_samples = speech_samples
+        self.speech_sample_rate = speech_sample_rate
+        self.final = final
diff --git a/seamless-server/src/transcoder_helpers.py b/seamless-server/src/transcoder_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..774b5b323d4745d07b8f369a296da37a5f5b720a
--- /dev/null
+++ b/seamless-server/src/transcoder_helpers.py
@@ -0,0 +1,44 @@
+import logging
+
+logger = logging.getLogger("socketio_server_pubsub")
+
+
+def get_transcoder_output_events(transcoder) -> list:
+    speech_and_text_output = transcoder.get_buffered_output()
+    if speech_and_text_output is None:
+        logger.debug("No output from transcoder.get_buffered_output()")
+        return []
+
+    logger.debug(f"We DID get output from the transcoder! {speech_and_text_output}")
+
+    lat = None
+
+    events = []
+
+
+    if speech_and_text_output.speech_samples:
+        events.append(
+            {
+                "event": "translation_speech",
+                "payload": speech_and_text_output.speech_samples,
+                "sample_rate": speech_and_text_output.speech_sample_rate,
+            }
+        )
+
+    if speech_and_text_output.text:
+        events.append(
+            {
+                "event": "translation_text",
+                "payload": speech_and_text_output.text,
+            }
+        )
+
+    for e in events:
+        e["eos"] = speech_and_text_output.final
+
+    # if not latency_sent:
+    #     lat = transcoder.first_translation_time()
+    #     latency_sent = True
+    #     to_send["latency"] = lat
+
+    return events
diff --git a/seamless-server/src/transcriber.py b/seamless-server/src/transcriber.py
new file mode 100644
index 0000000000000000000000000000000000000000..aef7ebf453e3c47e4de42d0141637030933d4a6a
--- /dev/null
+++ b/seamless-server/src/transcriber.py
@@ -0,0 +1,128 @@
+from deepgram import DeepgramClient, LiveTranscriptionEvents, LiveOptions
+import asyncio
+import os
+from src.logging import initialize_logger
+import logging
+import threading
+import time
+
+logger = initialize_logger("transcriber", level=logging.INFO)
+
+options = LiveOptions(
+    model="nova-2",
+    language="en-US",
+    smart_format=True,
+    punctuate=True,
+    # smart_format=True,
+    sample_rate=48000,
+    interim_results=True,
+)
+
+
+class Transcriber:
+    def __init__(
+        self,
+    ):
+        self.deepgram_api_key = os.getenv("DEEPGRAM_API_KEY")
+        self.deepgram = None
+        self.dg_connection = None
+        self.audio_queue = asyncio.Queue()
+        self.stop_event = threading.Event()
+
+    def process_audio(self):
+        while not self.stop_event.is_set():
+            try:
+                if self.dg_connection is None:
+                    logger.info("returned from process")
+                    return
+
+                if self.audio_queue.empty():
+                    time.sleep(0.1)
+                    continue
+
+                data = self.audio_queue.get_nowait()
+                self.dg_connection.send(data)
+                self.audio_queue.task_done()
+                logger.info("sent data to deepgram")
+            except Exception as e:
+                logger.warning(f"Error while sending data: {e}")
+                break
+
+        logger.info("Audio processing thread is stopping")
+
+    def on_transcript(self, result, *args, **kwargs):
+        try:
+            sentence = result.channel.alternatives[0].transcript
+            logger.info(f"Transcription: {sentence}")
+        except Exception as e:
+            logger.warning(e)
+
+    def close_connection(self):
+        if self.dg_connection:
+            self.dg_connection.finish()
+            self.dg_connection = None
+            logger.info("finished deepgram connection")
+
+    def stop(self):
+        self.stop_event.set()
+        self.close_connection()
+        logger.info("Requested to stop the audio processing thread")
+
+    def on_close(self, *args, **kwargs):
+        logger.info("Deepgram connection closed")
+        self.dg_connection = None
+
+    def on_utterance_end(self, utterance_end, *args, **kwargs):
+        logger.info(f"\n\n{utterance_end}\n\n")
+
+    def on_error(self, e, *args, **kwargs):
+        logger.warning(f"Deepgram error received {e}")
+        self.dg_connection = None
+
+    def start_deepgram(self):
+        try:
+            self.deepgram = DeepgramClient(self.deepgram_api_key)
+            dg_connection = self.deepgram.listen.live.v("1")
+        except Exception as e:
+            logger.warning(f"Could not open socket: {e}")
+            return
+
+        def on_message(self, result, **kwargs):
+            sentence = result.channel.alternatives[0].transcript
+            if len(sentence) == 0:
+                return
+            logger.info(f"speaker: {sentence}")
+
+        def on_metadata(self, metadata, **kwargs):
+            logger.info(f"\n\n{metadata}\n\n")
+
+        def on_utterance_end(self, utterance_end, **kwargs):
+            logger.info(f"\n\n{utterance_end}\n\n")
+
+        def on_error(self, error, **kwargs):
+            logger.info(f"\n\n{error}\n\n")
+
+        def on_close(self, **kwargs):
+            logger.info(f"\n\nclosed\n\n")
+
+        dg_connection.on(LiveTranscriptionEvents.Transcript, on_message)
+        dg_connection.on(LiveTranscriptionEvents.Metadata, on_metadata)
+        # dg_connection.on(LiveTranscriptionEvents.SpeechStarted, on_speech_started)
+        dg_connection.on(LiveTranscriptionEvents.UtteranceEnd, on_utterance_end)
+        dg_connection.on(LiveTranscriptionEvents.Error, on_error)
+        dg_connection.on(LiveTranscriptionEvents.Close, on_close)
+
+        dg_connection.start(options)
+        self.dg_connection = dg_connection
+
+        logger.info("deepgram connection opened")
+        self.process_audio()
+
+    def start(self):
+        threading.Thread(target=self.start_deepgram).start()
+
+    def send_audio(self, data):
+        try:
+            self.audio_queue.put_nowait(data)
+        except Exception as e:
+            logger.warning(e)
diff --git a/seamless-server/src/translate.py b/seamless-server/src/translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9102e800f0f81c14ac40199bc8fe9edf7ab6213
--- /dev/null
+++ b/seamless-server/src/translate.py
@@ -0,0 +1,21 @@
+import torch
+from seamless_communication.inference import Translator
+
+
+# Initialize a Translator object with a multitask model, vocoder on the GPU.
+translator = Translator(
+    "seamlessM4T_v2_large", "vocoder_v2", torch.device("cuda:0"), torch.float16
+)
+
+
+def translate_text(text):
+    print("test")
+    # text_output, speech_output = translator.predict(
+    #     input=text,
+    #     task_str="T2ST",
+    #     tgt_lang="spa",
+    #     src_lang="eng",
+    #     text_generation_opts=None,
+    #     unit_generation_opts=None,
+    # )
+    # print(text_output)
diff --git a/seamless-server/whl/seamless_communication-1.0.0-py3-none-any.whl b/seamless-server/whl/seamless_communication-1.0.0-py3-none-any.whl
new file mode 100644
index 0000000000000000000000000000000000000000..29a3b09e778e853532689eb0f72e96b035ea92f4
Binary files /dev/null and b/seamless-server/whl/seamless_communication-1.0.0-py3-none-any.whl differ
diff --git a/streaming-test-app/.eslintrc.cjs b/streaming-test-app/.eslintrc.cjs
new file mode 100644
index 0000000000000000000000000000000000000000..b2106c966499a1f9306b8e0247ac9aba219f4628
--- /dev/null
+++ b/streaming-test-app/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+  root: true,
+  env: {browser: true, es2020: true},
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react-hooks/recommended',
+  ],
+  ignorePatterns: ['dist', '.eslintrc.cjs'],
+  parser: '@typescript-eslint/parser',
+  plugins: ['react-refresh'],
+  rules: {
+    'react-refresh/only-export-components': [
+      'warn',
+      {allowConstantExport: true},
+    ],
+  },
+};
diff --git a/streaming-test-app/.gitignore b/streaming-test-app/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1
--- /dev/null
+++ b/streaming-test-app/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/streaming-test-app/index.html b/streaming-test-app/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..944528b2aac5d205441554a7816ed66e3ac632e3
--- /dev/null
+++ b/streaming-test-app/index.html
@@ -0,0 +1,13 @@
+
+
+  
+    Seamless Translation 
+  
+  
+    
+    
+  
+
diff --git a/streaming-test-app/package-lock.json b/streaming-test-app/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..2f7683dcc2f5589d0bb1c5224efc6f08573a23d5
--- /dev/null
+++ b/streaming-test-app/package-lock.json
@@ -0,0 +1,3491 @@
+{
+  "name": "streaming-test-app",
+  "version": "0.0.13",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "dev": true
+    },
+    "@ampproject/remapping": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
+      "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/gen-mapping": "^0.3.0",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      }
+    },
+    "@aws-crypto/sha256-js": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz",
+      "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==",
+      "requires": {
+        "@aws-crypto/util": "^1.2.2",
+        "@aws-sdk/types": "^3.1.0",
+        "tslib": "^1.11.1"
+      }
+    },
+    "@aws-crypto/util": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz",
+      "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==",
+      "requires": {
+        "@aws-sdk/types": "^3.1.0",
+        "@aws-sdk/util-utf8-browser": "^3.0.0",
+        "tslib": "^1.11.1"
+      }
+    },
+    "@aws-sdk/types": {
+      "version": "3.460.0",
+      "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.460.0.tgz",
+      "integrity": "sha512-MyZSWS/FV8Bnux5eD9en7KLgVxevlVrGNEP3X2D7fpnUlLhl0a7k8+OpSI2ozEQB8hIU2DLc/XXTKRerHSefxQ==",
+      "requires": {
+        "@smithy/types": "^2.5.0",
+        "tslib": "^2.5.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+          "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+        }
+      }
+    },
+    "@aws-sdk/util-utf8-browser": {
+      "version": "3.259.0",
+      "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz",
+      "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==",
+      "requires": {
+        "tslib": "^2.3.1"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+          "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+        }
+      }
+    },
+    "@babel/code-frame": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
+      "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
+      "requires": {
+        "@babel/highlight": "^7.23.4",
+        "chalk": "^2.4.2"
+      }
+    },
+    "@babel/compat-data": {
+      "version": "7.23.3",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz",
+      "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==",
+      "dev": true
+    },
+    "@babel/core": {
+      "version": "7.23.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz",
+      "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==",
+      "dev": true,
+      "requires": {
+        "@ampproject/remapping": "^2.2.0",
+        "@babel/code-frame": "^7.22.13",
+        "@babel/generator": "^7.23.3",
+        "@babel/helper-compilation-targets": "^7.22.15",
+        "@babel/helper-module-transforms": "^7.23.3",
+        "@babel/helpers": "^7.23.2",
+        "@babel/parser": "^7.23.3",
+        "@babel/template": "^7.22.15",
+        "@babel/traverse": "^7.23.3",
+        "@babel/types": "^7.23.3",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+          "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+          "dev": true
+        },
+        "semver": {
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/generator": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz",
+      "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.23.4",
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jsesc": "^2.5.1"
+      }
+    },
+    "@babel/helper-compilation-targets": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
+      "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+      "dev": true,
+      "requires": {
+        "@babel/compat-data": "^7.22.9",
+        "@babel/helper-validator-option": "^7.22.15",
+        "browserslist": "^4.21.9",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+          "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+          "dev": true,
+          "requires": {
+            "yallist": "^3.0.2"
+          }
+        },
+        "semver": {
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+          "dev": true
+        },
+        "yallist": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+          "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-environment-visitor": {
+      "version": "7.22.20",
+      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+      "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
+      "dev": true
+    },
+    "@babel/helper-function-name": {
+      "version": "7.23.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+      "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.22.15",
+        "@babel/types": "^7.23.0"
+      }
+    },
+    "@babel/helper-hoist-variables": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+      "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.22.5"
+      }
+    },
+    "@babel/helper-module-imports": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
+      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+      "requires": {
+        "@babel/types": "^7.22.15"
+      }
+    },
+    "@babel/helper-module-transforms": {
+      "version": "7.23.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
+      "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-environment-visitor": "^7.22.20",
+        "@babel/helper-module-imports": "^7.22.15",
+        "@babel/helper-simple-access": "^7.22.5",
+        "@babel/helper-split-export-declaration": "^7.22.6",
+        "@babel/helper-validator-identifier": "^7.22.20"
+      }
+    },
+    "@babel/helper-plugin-utils": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
+      "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
+      "dev": true
+    },
+    "@babel/helper-simple-access": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
+      "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.22.5"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.22.5"
+      }
+    },
+    "@babel/helper-string-parser": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ=="
+    },
+    "@babel/helper-validator-identifier": {
+      "version": "7.22.20",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
+    },
+    "@babel/helper-validator-option": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
+      "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+      "dev": true
+    },
+    "@babel/helpers": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz",
+      "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.22.15",
+        "@babel/traverse": "^7.23.4",
+        "@babel/types": "^7.23.4"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "chalk": "^2.4.2",
+        "js-tokens": "^4.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
+      "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
+      "dev": true
+    },
+    "@babel/plugin-transform-react-jsx-self": {
+      "version": "7.23.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz",
+      "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.22.5"
+      }
+    },
+    "@babel/plugin-transform-react-jsx-source": {
+      "version": "7.23.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz",
+      "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.22.5"
+      }
+    },
+    "@babel/runtime": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz",
+      "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==",
+      "requires": {
+        "regenerator-runtime": "^0.14.0"
+      }
+    },
+    "@babel/template": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.22.13",
+        "@babel/parser": "^7.22.15",
+        "@babel/types": "^7.22.15"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz",
+      "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.23.4",
+        "@babel/generator": "^7.23.4",
+        "@babel/helper-environment-visitor": "^7.22.20",
+        "@babel/helper-function-name": "^7.23.0",
+        "@babel/helper-hoist-variables": "^7.22.5",
+        "@babel/helper-split-export-declaration": "^7.22.6",
+        "@babel/parser": "^7.23.4",
+        "@babel/types": "^7.23.4",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0"
+      }
+    },
+    "@babel/types": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz",
+      "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==",
+      "requires": {
+        "@babel/helper-string-parser": "^7.23.4",
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@emotion/babel-plugin": {
+      "version": "11.11.0",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+      "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
+      "requires": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.1",
+        "@emotion/memoize": "^0.8.1",
+        "@emotion/serialize": "^1.1.2",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.2.0"
+      }
+    },
+    "@emotion/cache": {
+      "version": "11.11.0",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
+      "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
+      "requires": {
+        "@emotion/memoize": "^0.8.1",
+        "@emotion/sheet": "^1.2.2",
+        "@emotion/utils": "^1.2.1",
+        "@emotion/weak-memoize": "^0.3.1",
+        "stylis": "4.2.0"
+      }
+    },
+    "@emotion/hash": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
+      "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
+    },
+    "@emotion/is-prop-valid": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+      "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+      "requires": {
+        "@emotion/memoize": "^0.8.1"
+      }
+    },
+    "@emotion/memoize": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+      "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+    },
+    "@emotion/react": {
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
+      "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.11.0",
+        "@emotion/cache": "^11.11.0",
+        "@emotion/serialize": "^1.1.2",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+        "@emotion/utils": "^1.2.1",
+        "@emotion/weak-memoize": "^0.3.1",
+        "hoist-non-react-statics": "^3.3.1"
+      }
+    },
+    "@emotion/serialize": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz",
+      "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==",
+      "requires": {
+        "@emotion/hash": "^0.9.1",
+        "@emotion/memoize": "^0.8.1",
+        "@emotion/unitless": "^0.8.1",
+        "@emotion/utils": "^1.2.1",
+        "csstype": "^3.0.2"
+      }
+    },
+    "@emotion/sheet": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
+      "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
+    },
+    "@emotion/styled": {
+      "version": "11.11.0",
+      "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
+      "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.11.0",
+        "@emotion/is-prop-valid": "^1.2.1",
+        "@emotion/serialize": "^1.1.2",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+        "@emotion/utils": "^1.2.1"
+      }
+    },
+    "@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+    },
+    "@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
+      "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw=="
+    },
+    "@emotion/utils": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
+      "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
+    },
+    "@emotion/weak-memoize": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
+      "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
+    },
+    "@esbuild/android-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-loong64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-mips64el": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ppc64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-riscv64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-s390x": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/netbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/openbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/sunos-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^3.3.0"
+      }
+    },
+    "@eslint-community/regexpp": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+      "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+      "dev": true
+    },
+    "@eslint/eslintrc": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz",
+      "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "globals": {
+          "version": "13.23.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+          "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+          "dev": true,
+          "requires": {
+            "type-fest": "^0.20.2"
+          }
+        }
+      }
+    },
+    "@eslint/js": {
+      "version": "8.54.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
+      "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
+      "dev": true
+    },
+    "@humanwhocodes/config-array": {
+      "version": "0.11.13",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+      "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+      "dev": true,
+      "requires": {
+        "@humanwhocodes/object-schema": "^2.0.1",
+        "debug": "^4.1.1",
+        "minimatch": "^3.0.5"
+      }
+    },
+    "@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true
+    },
+    "@humanwhocodes/object-schema": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+      "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+      "dev": true
+    },
+    "@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      }
+    },
+    "@jridgewell/resolve-uri": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "dev": true
+    },
+    "@jridgewell/set-array": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "dev": true
+    },
+    "@jridgewell/sourcemap-codec": {
+      "version": "1.4.15",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+      "dev": true
+    },
+    "@jridgewell/trace-mapping": {
+      "version": "0.3.20",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
+      "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "@mediapipe/tasks-vision": {
+      "version": "0.10.8",
+      "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
+      "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q=="
+    },
+    "@mui/base": {
+      "version": "5.0.0-beta.11",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.11.tgz",
+      "integrity": "sha512-FdKZGPd8qmC3ZNke7CNhzcEgToc02M6WYZc9hcBsNQ17bgAd3s9F//1bDDYgMVBYxDM71V0sv/hBHlOY4I1ZVA==",
+      "requires": {
+        "@babel/runtime": "^7.22.6",
+        "@emotion/is-prop-valid": "^1.2.1",
+        "@mui/types": "^7.2.4",
+        "@mui/utils": "^5.14.5",
+        "@popperjs/core": "^2.11.8",
+        "clsx": "^2.0.0",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "18.2.0",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+          "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+        }
+      }
+    },
+    "@mui/core-downloads-tracker": {
+      "version": "5.14.18",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.18.tgz",
+      "integrity": "sha512-yFpF35fEVDV81nVktu0BE9qn2dD/chs7PsQhlyaV3EnTeZi9RZBuvoEfRym1/jmhJ2tcfeWXiRuHG942mQXJJQ=="
+    },
+    "@mui/icons-material": {
+      "version": "5.14.3",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.3.tgz",
+      "integrity": "sha512-XkxWPhageu1OPUm2LWjo5XqeQ0t2xfGe8EiLkRW9oz2LHMMZmijvCxulhgquUVTF1DnoSh+3KoDLSsoAFtVNVw==",
+      "requires": {
+        "@babel/runtime": "^7.22.6"
+      }
+    },
+    "@mui/material": {
+      "version": "5.14.5",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.5.tgz",
+      "integrity": "sha512-4qa4GMfuZH0Ai3mttk5ccXP8a3sf7aPlAJwyMrUSz6h9hPri6BPou94zeu3rENhhmKLby9S/W1y+pmficy8JKA==",
+      "requires": {
+        "@babel/runtime": "^7.22.6",
+        "@mui/base": "5.0.0-beta.11",
+        "@mui/core-downloads-tracker": "^5.14.5",
+        "@mui/system": "^5.14.5",
+        "@mui/types": "^7.2.4",
+        "@mui/utils": "^5.14.5",
+        "@types/react-transition-group": "^4.4.6",
+        "clsx": "^2.0.0",
+        "csstype": "^3.1.2",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0",
+        "react-transition-group": "^4.4.5"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "18.2.0",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+          "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+        }
+      }
+    },
+    "@mui/private-theming": {
+      "version": "5.14.18",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.18.tgz",
+      "integrity": "sha512-WSgjqRlzfHU+2Rou3HlR2Gqfr4rZRsvFgataYO3qQ0/m6gShJN+lhVEvwEiJ9QYyVzMDvNpXZAcqp8Y2Vl+PAw==",
+      "requires": {
+        "@babel/runtime": "^7.23.2",
+        "@mui/utils": "^5.14.18",
+        "prop-types": "^15.8.1"
+      }
+    },
+    "@mui/styled-engine": {
+      "version": "5.14.18",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.18.tgz",
+      "integrity": "sha512-pW8bpmF9uCB5FV2IPk6mfbQCjPI5vGI09NOLhtGXPeph/4xIfC3JdIX0TILU0WcTs3aFQqo6s2+1SFgIB9rCXA==",
+      "requires": {
+        "@babel/runtime": "^7.23.2",
+        "@emotion/cache": "^11.11.0",
+        "csstype": "^3.1.2",
+        "prop-types": "^15.8.1"
+      }
+    },
+    "@mui/system": {
+      "version": "5.14.18",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.18.tgz",
+      "integrity": "sha512-hSQQdb3KF72X4EN2hMEiv8EYJZSflfdd1TRaGPoR7CIAG347OxCslpBUwWngYobaxgKvq6xTrlIl+diaactVww==",
+      "requires": {
+        "@babel/runtime": "^7.23.2",
+        "@mui/private-theming": "^5.14.18",
+        "@mui/styled-engine": "^5.14.18",
+        "@mui/types": "^7.2.9",
+        "@mui/utils": "^5.14.18",
+        "clsx": "^2.0.0",
+        "csstype": "^3.1.2",
+        "prop-types": "^15.8.1"
+      }
+    },
+    "@mui/types": {
+      "version": "7.2.9",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.9.tgz",
+      "integrity": "sha512-k1lN/PolaRZfNsRdAqXtcR71sTnv3z/VCCGPxU8HfdftDkzi335MdJ6scZxvofMAd/K/9EbzCZTFBmlNpQVdCg=="
+    },
+    "@mui/utils": {
+      "version": "5.14.18",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.18.tgz",
+      "integrity": "sha512-HZDRsJtEZ7WMSnrHV9uwScGze4wM/Y+u6pDVo+grUjt5yXzn+wI8QX/JwTHh9YSw/WpnUL80mJJjgCnWj2VrzQ==",
+      "requires": {
+        "@babel/runtime": "^7.23.2",
+        "@types/prop-types": "^15.7.10",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "18.2.0",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+          "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+        }
+      }
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
+    "@popperjs/core": {
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
+    },
+    "@react-spring/animated": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
+      "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
+      "requires": {
+        "@react-spring/shared": "~9.6.1",
+        "@react-spring/types": "~9.6.1"
+      }
+    },
+    "@react-spring/core": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
+      "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
+      "requires": {
+        "@react-spring/animated": "~9.6.1",
+        "@react-spring/rafz": "~9.6.1",
+        "@react-spring/shared": "~9.6.1",
+        "@react-spring/types": "~9.6.1"
+      }
+    },
+    "@react-spring/rafz": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
+      "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ=="
+    },
+    "@react-spring/shared": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
+      "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
+      "requires": {
+        "@react-spring/rafz": "~9.6.1",
+        "@react-spring/types": "~9.6.1"
+      }
+    },
+    "@react-spring/three": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
+      "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
+      "requires": {
+        "@react-spring/animated": "~9.6.1",
+        "@react-spring/core": "~9.6.1",
+        "@react-spring/shared": "~9.6.1",
+        "@react-spring/types": "~9.6.1"
+      }
+    },
+    "@react-spring/types": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
+      "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q=="
+    },
+    "@react-three/drei": {
+      "version": "9.89.0",
+      "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.89.0.tgz",
+      "integrity": "sha512-iddG7OAfng3a99nAO7erzb1wlwaY1Zlaz9EVfnW9ZiLm4rM322vXHnLX1zhrH5/AeRB/1t4bEu8uAHvXX3XwWA==",
+      "requires": {
+        "@babel/runtime": "^7.11.2",
+        "@mediapipe/tasks-vision": "0.10.8",
+        "@react-spring/three": "~9.6.1",
+        "@use-gesture/react": "^10.2.24",
+        "camera-controls": "^2.4.2",
+        "cross-env": "^7.0.3",
+        "detect-gpu": "^5.0.28",
+        "glsl-noise": "^0.0.0",
+        "lodash.clamp": "^4.0.3",
+        "lodash.omit": "^4.5.0",
+        "lodash.pick": "^4.4.0",
+        "maath": "^0.9.0",
+        "meshline": "^3.1.6",
+        "react-composer": "^5.0.3",
+        "react-merge-refs": "^1.1.0",
+        "stats-gl": "^1.0.4",
+        "stats.js": "^0.17.0",
+        "suspend-react": "^0.1.3",
+        "three-mesh-bvh": "^0.6.7",
+        "three-stdlib": "^2.28.0",
+        "troika-three-text": "^0.47.2",
+        "utility-types": "^3.10.0",
+        "uuid": "^9.0.1",
+        "zustand": "^3.5.13"
+      },
+      "dependencies": {
+        "zustand": {
+          "version": "3.7.2",
+          "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+          "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="
+        }
+      }
+    },
+    "@react-three/fiber": {
+      "version": "8.15.11",
+      "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.15.11.tgz",
+      "integrity": "sha512-jOJjrjVMBJQwIK6Uirc3bErUCTiclbS2alJG1eU8pV1jIwDZwPwcfHzSi2TautxoA4ddMt5DmlpatK4rIqM4jA==",
+      "requires": {
+        "@babel/runtime": "^7.17.8",
+        "@types/react-reconciler": "^0.26.7",
+        "@types/webxr": "*",
+        "base64-js": "^1.5.1",
+        "buffer": "^6.0.3",
+        "its-fine": "^1.0.6",
+        "react-reconciler": "^0.27.0",
+        "react-use-measure": "^2.1.1",
+        "scheduler": "^0.21.0",
+        "suspend-react": "^0.1.3",
+        "zustand": "^3.7.1"
+      },
+      "dependencies": {
+        "zustand": {
+          "version": "3.7.2",
+          "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+          "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="
+        }
+      }
+    },
+    "@react-three/xr": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/@react-three/xr/-/xr-5.7.1.tgz",
+      "integrity": "sha512-GaRUSA+lE8VJF/NrXq7QQByZ4UGHbQQ4rs3QCphZs9fVidK86hGrMOQ0kL79gZc5pa3V5uFGlOhNcUdsTYE3Bg==",
+      "requires": {
+        "@types/webxr": "*",
+        "three-stdlib": "^2.21.1",
+        "zustand": "^3.7.1"
+      },
+      "dependencies": {
+        "zustand": {
+          "version": "3.7.2",
+          "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+          "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="
+        }
+      }
+    },
+    "@smithy/types": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.6.0.tgz",
+      "integrity": "sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==",
+      "requires": {
+        "tslib": "^2.5.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+          "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+        }
+      }
+    },
+    "@socket.io/component-emitter": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
+    },
+    "@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "requires": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "@types/babel__generator": {
+      "version": "7.6.7",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz",
+      "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "requires": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@types/babel__traverse": {
+      "version": "7.20.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz",
+      "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.20.7"
+      }
+    },
+    "@types/draco3d": {
+      "version": "1.4.9",
+      "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.9.tgz",
+      "integrity": "sha512-4MMUjMQb4yA5fJ4osXx+QxGHt0/ZSy4spT6jL1HM7Tn8OJEC35siqdnpOo+HxPhYjqEFumKfGVF9hJfdyKBIBA=="
+    },
+    "@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "20.10.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
+      "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
+      "dev": true,
+      "requires": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "@types/offscreencanvas": {
+      "version": "2019.7.3",
+      "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+      "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
+    },
+    "@types/parse-json": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+      "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
+    },
+    "@types/prop-types": {
+      "version": "15.7.11",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
+      "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
+    },
+    "@types/react": {
+      "version": "18.2.39",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
+      "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
+      "requires": {
+        "@types/prop-types": "*",
+        "@types/scheduler": "*",
+        "csstype": "^3.0.2"
+      }
+    },
+    "@types/react-dom": {
+      "version": "18.2.17",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
+      "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*"
+      }
+    },
+    "@types/react-reconciler": {
+      "version": "0.26.7",
+      "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
+      "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
+    "@types/react-transition-group": {
+      "version": "4.4.9",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.9.tgz",
+      "integrity": "sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
+    "@types/scheduler": {
+      "version": "0.16.8",
+      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
+      "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
+    },
+    "@types/semver": {
+      "version": "7.5.6",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
+      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+      "dev": true
+    },
+    "@types/uuid": {
+      "version": "9.0.7",
+      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
+      "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==",
+      "dev": true
+    },
+    "@types/webxr": {
+      "version": "0.5.10",
+      "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.10.tgz",
+      "integrity": "sha512-n3u5sqXQJhf1CS68mw3Wf16FQ4cRPNBBwdYLFzq3UddiADOim1Pn3Y6PBdDilz1vOJF3ybLxJ8ZEDlLIzrOQZg=="
+    },
+    "@typescript-eslint/eslint-plugin": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+      "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/type-utils": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+      "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
+        "debug": "^4.3.4"
+      }
+    },
+    "@typescript-eslint/scope-manager": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
+      }
+    },
+    "@typescript-eslint/type-utils": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+      "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      }
+    },
+    "@typescript-eslint/types": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+      "dev": true
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      }
+    },
+    "@typescript-eslint/utils": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+      "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "semver": "^7.5.4"
+      }
+    },
+    "@typescript-eslint/visitor-keys": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "6.13.1",
+        "eslint-visitor-keys": "^3.4.1"
+      }
+    },
+    "@ungap/structured-clone": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+      "dev": true
+    },
+    "@use-gesture/core": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.0.tgz",
+      "integrity": "sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A=="
+    },
+    "@use-gesture/react": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.0.tgz",
+      "integrity": "sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==",
+      "requires": {
+        "@use-gesture/core": "10.3.0"
+      }
+    },
+    "@vitejs/plugin-react": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.0.tgz",
+      "integrity": "sha512-+MHTH/e6H12kRp5HUkzOGqPMksezRMmW+TNzlh/QXfI8rRf6l2Z2yH/v12no1UvTwhZgEDMuQ7g7rrfMseU6FQ==",
+      "dev": true,
+      "requires": {
+        "@babel/core": "^7.23.3",
+        "@babel/plugin-transform-react-jsx-self": "^7.23.3",
+        "@babel/plugin-transform-react-jsx-source": "^7.23.3",
+        "@types/babel__core": "^7.20.4",
+        "react-refresh": "^0.14.0"
+      }
+    },
+    "acorn": {
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+      "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true
+    },
+    "ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "amazon-cognito-identity-js": {
+      "version": "6.3.7",
+      "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.7.tgz",
+      "integrity": "sha512-tSjnM7KyAeOZ7UMah+oOZ6cW4Gf64FFcc7BE2l7MTcp7ekAPrXaCbpcW2xEpH1EiDS4cPcAouHzmCuc2tr72vQ==",
+      "requires": {
+        "@aws-crypto/sha256-js": "1.2.2",
+        "buffer": "4.9.2",
+        "fast-base64-decode": "^1.0.0",
+        "isomorphic-unfetch": "^3.0.0",
+        "js-cookie": "^2.2.1"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "4.9.2",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+          "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+          "requires": {
+            "base64-js": "^1.0.2",
+            "ieee754": "^1.1.4",
+            "isarray": "^1.0.0"
+          }
+        },
+        "js-cookie": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
+          "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
+        }
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true
+    },
+    "audiobuffer-to-wav": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/audiobuffer-to-wav/-/audiobuffer-to-wav-1.0.0.tgz",
+      "integrity": "sha512-CAoir4NRrAzAgYo20tEMiKZR84coE8bq/L+H2kwAaULVY4+0xySsEVtNT5raqpzmH6y0pqzY6EmoViLd9W8F/w=="
+    },
+    "available-typed-arrays": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
+    },
+    "aws-sdk": {
+      "version": "2.1506.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1506.0.tgz",
+      "integrity": "sha512-jSBbofvPa7HJykyM7Xph9psMcWPl6UgdiKjG2E7fHJb6psW+BZN9ZvSGOBvRIlT8Y6+JGzI0qkouS1OLK9slhg==",
+      "requires": {
+        "buffer": "4.9.2",
+        "events": "1.1.1",
+        "ieee754": "1.1.13",
+        "jmespath": "0.16.0",
+        "querystring": "0.2.0",
+        "sax": "1.2.1",
+        "url": "0.10.3",
+        "util": "^0.12.4",
+        "uuid": "8.0.0",
+        "xml2js": "0.5.0"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "4.9.2",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+          "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+          "requires": {
+            "base64-js": "^1.0.2",
+            "ieee754": "^1.1.4",
+            "isarray": "^1.0.0"
+          }
+        },
+        "ieee754": {
+          "version": "1.1.13",
+          "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+          "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+        },
+        "uuid": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
+          "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="
+        }
+      }
+    },
+    "babel-plugin-macros": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+      "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "cosmiconfig": "^7.0.0",
+        "resolve": "^1.19.0"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+    },
+    "bidi-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+      "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+      "requires": {
+        "require-from-string": "^2.0.2"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browserslist": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
+      "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30001541",
+        "electron-to-chromium": "^1.4.535",
+        "node-releases": "^2.0.13",
+        "update-browserslist-db": "^1.0.13"
+      }
+    },
+    "buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "requires": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "call-bind": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "requires": {
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.1",
+        "set-function-length": "^1.1.1"
+      }
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+    },
+    "camera-controls": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.7.3.tgz",
+      "integrity": "sha512-L4mxjBd3u8qiOLozdWrH2P8ZybSsDXBF7iyNyqNEFJhPUkovmuARWR8JTc1B/qlclOIg6FvZZA/0uAZMMim0mw=="
+    },
+    "caniuse-lite": {
+      "version": "1.0.30001565",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
+      "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "dependencies": {
+        "escape-string-regexp": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
+        }
+      }
+    },
+    "cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "requires": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "clsx": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+      "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "concurrently": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.1.tgz",
+      "integrity": "sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.2",
+        "date-fns": "^2.30.0",
+        "lodash": "^4.17.21",
+        "rxjs": "^7.8.1",
+        "shell-quote": "^1.8.1",
+        "spawn-command": "0.0.2",
+        "supports-color": "^8.1.1",
+        "tree-kill": "^1.2.2",
+        "yargs": "^17.7.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          },
+          "dependencies": {
+            "supports-color": {
+              "version": "7.2.0",
+              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+              "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+              "dev": true,
+              "requires": {
+                "has-flag": "^4.0.0"
+              }
+            }
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "8.1.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+          "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "convert-source-map": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+    },
+    "cosmiconfig": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+      "requires": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.2.1",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.10.0"
+      }
+    },
+    "cross-env": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+      "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+      "requires": {
+        "cross-spawn": "^7.0.1"
+      }
+    },
+    "cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
+    "csstype": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+    },
+    "date-fns": {
+      "version": "2.30.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+      "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.21.0"
+      }
+    },
+    "debounce": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
+      "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
+    },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
+    "deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
+    },
+    "define-data-property": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "requires": {
+        "get-intrinsic": "^1.2.1",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.0"
+      }
+    },
+    "detect-gpu": {
+      "version": "5.0.37",
+      "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.37.tgz",
+      "integrity": "sha512-EraWs84faI4iskB4qvE39bevMIazEvd1RpoyGLOBesRLbiz6eMeJqqRPHjEFClfRByYZzi9IzU35rBXIO76oDw==",
+      "requires": {
+        "webgl-constants": "^1.1.1"
+      }
+    },
+    "dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "requires": {
+        "path-type": "^4.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "requires": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "draco3d": {
+      "version": "1.5.6",
+      "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.6.tgz",
+      "integrity": "sha512-+3NaRjWktb5r61ZFoDejlykPEFKT5N/LkbXsaddlw6xNSXBanUYpFc2AXXpbJDilPHazcSreU/DpQIaxfX0NfQ=="
+    },
+    "electron-to-chromium": {
+      "version": "1.4.596",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.596.tgz",
+      "integrity": "sha512-zW3zbZ40Icb2BCWjm47nxwcFGYlIgdXkAx85XDO7cyky9J4QQfq8t0W19/TLZqq3JPQXtlv8BPIGmfa9Jb4scg==",
+      "dev": true
+    },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "engine.io-client": {
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
+      "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
+      "requires": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.11.0",
+        "xmlhttprequest-ssl": "~2.0.0"
+      }
+    },
+    "engine.io-parser": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
+      "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "esbuild": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+      "dev": true,
+      "requires": {
+        "@esbuild/android-arm": "0.18.20",
+        "@esbuild/android-arm64": "0.18.20",
+        "@esbuild/android-x64": "0.18.20",
+        "@esbuild/darwin-arm64": "0.18.20",
+        "@esbuild/darwin-x64": "0.18.20",
+        "@esbuild/freebsd-arm64": "0.18.20",
+        "@esbuild/freebsd-x64": "0.18.20",
+        "@esbuild/linux-arm": "0.18.20",
+        "@esbuild/linux-arm64": "0.18.20",
+        "@esbuild/linux-ia32": "0.18.20",
+        "@esbuild/linux-loong64": "0.18.20",
+        "@esbuild/linux-mips64el": "0.18.20",
+        "@esbuild/linux-ppc64": "0.18.20",
+        "@esbuild/linux-riscv64": "0.18.20",
+        "@esbuild/linux-s390x": "0.18.20",
+        "@esbuild/linux-x64": "0.18.20",
+        "@esbuild/netbsd-x64": "0.18.20",
+        "@esbuild/openbsd-x64": "0.18.20",
+        "@esbuild/sunos-x64": "0.18.20",
+        "@esbuild/win32-arm64": "0.18.20",
+        "@esbuild/win32-ia32": "0.18.20",
+        "@esbuild/win32-x64": "0.18.20"
+      }
+    },
+    "escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+    },
+    "eslint": {
+      "version": "8.54.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
+      "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.3",
+        "@eslint/js": "8.54.0",
+        "@humanwhocodes/config-array": "^0.11.13",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "glob-parent": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+          "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+          "dev": true,
+          "requires": {
+            "is-glob": "^4.0.3"
+          }
+        },
+        "globals": {
+          "version": "13.23.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+          "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+          "dev": true,
+          "requires": {
+            "type-fest": "^0.20.2"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "eslint-plugin-react-hooks": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
+      "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+      "dev": true
+    },
+    "eslint-plugin-react-refresh": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.4.tgz",
+      "integrity": "sha512-eD83+65e8YPVg6603Om2iCIwcQJf/y7++MWm4tACtEswFLYMwxwVWAfwN+e19f5Ad/FOyyNg9Dfi5lXhH3Y3rA==",
+      "dev": true
+    },
+    "eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true
+    },
+    "espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "requires": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      }
+    },
+    "esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.1.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.2.0"
+      }
+    },
+    "estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="
+    },
+    "fast-base64-decode": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
+      "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q=="
+    },
+    "fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
+    },
+    "fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "fflate": {
+      "version": "0.6.10",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+      "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="
+    },
+    "file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^3.0.4"
+      }
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-root": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+    },
+    "find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "requires": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+      "dev": true,
+      "requires": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      }
+    },
+    "flatted": {
+      "version": "3.2.9",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+      "dev": true
+    },
+    "for-each": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "requires": {
+        "is-callable": "^1.1.3"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "optional": true
+    },
+    "function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true
+    },
+    "get-intrinsic": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+      "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+      "requires": {
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "globals": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "dev": true
+    },
+    "globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "dev": true,
+      "requires": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      }
+    },
+    "glsl-noise": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+      "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w=="
+    },
+    "gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "requires": {
+        "get-intrinsic": "^1.1.3"
+      }
+    },
+    "graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
+    },
+    "has-property-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "requires": {
+        "get-intrinsic": "^1.2.2"
+      }
+    },
+    "has-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
+    },
+    "has-symbols": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+    },
+    "has-tostringtag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "requires": {
+        "has-symbols": "^1.0.2"
+      }
+    },
+    "hasown": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
+    "hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "requires": {
+        "react-is": "^16.7.0"
+      }
+    },
+    "ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+    },
+    "ignore": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+      "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+      "dev": true
+    },
+    "import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "is-arguments": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "requires": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+    },
+    "is-callable": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="
+    },
+    "is-core-module": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "requires": {
+        "hasown": "^2.0.0"
+      }
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true
+    },
+    "is-generator-function": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "requires": {
+        "has-tostringtag": "^1.0.0"
+      }
+    },
+    "is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true
+    },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true
+    },
+    "is-typed-array": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+      "requires": {
+        "which-typed-array": "^1.1.11"
+      }
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "iso-639-1": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.0.tgz",
+      "integrity": "sha512-rWcHp9dcNbxa5C8jA/cxFlWNFNwy5Vup0KcFvgA8sPQs9ZeJHj/Eq0Y8Yz2eL8XlWYpxw4iwh9FfTeVxyqdRMw=="
+    },
+    "isomorphic-unfetch": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
+      "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
+      "requires": {
+        "node-fetch": "^2.6.1",
+        "unfetch": "^4.2.0"
+      }
+    },
+    "its-fine": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.1.1.tgz",
+      "integrity": "sha512-v1Ia1xl20KbuSGlwoaGsW0oxsw8Be+TrXweidxD9oT/1lAh6O3K3/GIM95Tt6WCiv6W+h2M7RB1TwdoAjQyyKw==",
+      "requires": {
+        "@types/react-reconciler": "^0.28.0"
+      },
+      "dependencies": {
+        "@types/react-reconciler": {
+          "version": "0.28.8",
+          "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz",
+          "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==",
+          "requires": {
+            "@types/react": "*"
+          }
+        }
+      }
+    },
+    "jmespath": {
+      "version": "0.16.0",
+      "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
+      "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="
+    },
+    "js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "requires": {
+        "argparse": "^2.0.1"
+      }
+    },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "dev": true
+    },
+    "json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true
+    },
+    "json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true
+    },
+    "keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "requires": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      }
+    },
+    "lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+    },
+    "locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "requires": {
+        "p-locate": "^5.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "lodash.clamp": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
+      "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg=="
+    },
+    "lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "lodash.omit": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
+      "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg=="
+    },
+    "lodash.pick": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+      "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q=="
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "requires": {
+        "yallist": "^4.0.0"
+      }
+    },
+    "maath": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/maath/-/maath-0.9.0.tgz",
+      "integrity": "sha512-aAR8hoUqPxlsU8VOxkS9y37jhUzdUxM017NpCuxFU1Gk+nMaZASZxymZrV8LRSHzRk/watlbfyNKu6XPUhCFrQ=="
+    },
+    "merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true
+    },
+    "meshline": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.1.7.tgz",
+      "integrity": "sha512-uf9fPI9wy0Ie0kZjvKuIkf2n7gi3ih0wdTeb/kmSvmzpPyEL5d9lFohg9+JV9VC4sQUBOZDgxu6fnjn57goSHg=="
+    },
+    "micromatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "dev": true,
+      "requires": {
+        "braces": "^3.0.2",
+        "picomatch": "^2.3.1"
+      }
+    },
+    "minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "dev": true
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
+    "node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "requires": {
+        "whatwg-url": "^5.0.0"
+      }
+    },
+    "node-releases": {
+      "version": "2.0.13",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "optionator": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+      "dev": true,
+      "requires": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0"
+      }
+    },
+    "p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "requires": {
+        "yocto-queue": "^0.1.0"
+      }
+    },
+    "p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "requires": {
+        "p-limit": "^3.0.2"
+      }
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      }
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+    },
+    "path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+    },
+    "path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
+    },
+    "picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "dev": true
+    },
+    "picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true
+    },
+    "postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "dev": true,
+      "requires": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "potpack": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+      "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="
+    },
+    "prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true
+    },
+    "prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "requires": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "punycode": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+      "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="
+    },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
+    "react": {
+      "version": "18.2.0",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+      "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+      "requires": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "react-composer": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz",
+      "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==",
+      "requires": {
+        "prop-types": "^15.6.0"
+      }
+    },
+    "react-dom": {
+      "version": "18.2.0",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+      "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.0"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.23.0",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+          "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+          "requires": {
+            "loose-envify": "^1.1.0"
+          }
+        }
+      }
+    },
+    "react-google-charts": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/react-google-charts/-/react-google-charts-4.0.1.tgz",
+      "integrity": "sha512-V/hcMcNuBgD5w49BYTUDye+bUKaPmsU5vy/9W/Nj2xEeGn+6/AuH9IvBkbDcNBsY00cV9OeexdmgfI5RFHgsXQ=="
+    },
+    "react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
+    "react-merge-refs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
+      "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ=="
+    },
+    "react-reconciler": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
+      "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.21.0"
+      }
+    },
+    "react-refresh": {
+      "version": "0.14.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
+      "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+      "dev": true
+    },
+    "react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "requires": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      }
+    },
+    "react-use-measure": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz",
+      "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==",
+      "requires": {
+        "debounce": "^1.2.1"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.14.0",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
+      "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true
+    },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
+    },
+    "resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "requires": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "rollup": {
+      "version": "3.29.4",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
+      "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
+      "dev": true,
+      "requires": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "rxjs": {
+      "version": "7.8.1",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+      "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+      "dev": true,
+      "requires": {
+        "tslib": "^2.1.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+          "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+          "dev": true
+        }
+      }
+    },
+    "sax": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+      "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="
+    },
+    "scheduler": {
+      "version": "0.21.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
+      "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
+      "requires": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "semver": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "dev": true,
+      "requires": {
+        "lru-cache": "^6.0.0"
+      }
+    },
+    "set-function-length": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
+      "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+      "requires": {
+        "define-data-property": "^1.1.1",
+        "get-intrinsic": "^1.2.1",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.0"
+      }
+    },
+    "shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+    },
+    "shell-quote": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+      "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+      "dev": true
+    },
+    "slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true
+    },
+    "socket.io-client": {
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
+      "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
+      "requires": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.2",
+        "engine.io-client": "~6.5.2",
+        "socket.io-parser": "~4.2.4"
+      }
+    },
+    "socket.io-parser": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "requires": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      }
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="
+    },
+    "source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "dev": true
+    },
+    "spawn-command": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
+      "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
+      "dev": true
+    },
+    "stats-gl": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-1.0.7.tgz",
+      "integrity": "sha512-vZI82CjefSxLC1bjw36z28v0+QE9rJKymGlXtfWu+ipW70ZEAwa4EbO4LxluAfLfpqiaAS04NzpYBRLDeAwYWQ=="
+    },
+    "stats.js": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+      "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="
+    },
+    "string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      }
+    },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
+    "strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true
+    },
+    "stylis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+      "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
+    },
+    "suspend-react": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+      "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true
+    },
+    "three": {
+      "version": "0.156.1",
+      "resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
+      "integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
+    },
+    "three-mesh-bvh": {
+      "version": "0.6.8",
+      "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.6.8.tgz",
+      "integrity": "sha512-EGebF9DZx1S8+7OZYNNTT80GXJZVf+UYXD/HyTg/e2kR/ApofIFfUS4ZzIHNnUVIadpnLSzM4n96wX+l7GMbnQ=="
+    },
+    "three-mesh-ui": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/three-mesh-ui/-/three-mesh-ui-6.5.4.tgz",
+      "integrity": "sha512-QA2KlHrbj7zGjKqpNXcwvini8g5gOXb44ExdupVpxCqmHln/sWNGOmN0yIa/HjnMGYJWP4M+NchTHvwUp+MWIw=="
+    },
+    "three-stdlib": {
+      "version": "2.28.7",
+      "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.28.7.tgz",
+      "integrity": "sha512-E7NuztilCswBKnEoyqydvA7N4dy0cf/gLA0bKrrg6+Q6j4WtusGa/+t9oK2HVq47S1AHRH2CvFHpdIGNjPKo/A==",
+      "requires": {
+        "@types/draco3d": "^1.4.0",
+        "@types/offscreencanvas": "^2019.6.4",
+        "@types/webxr": "^0.5.2",
+        "draco3d": "^1.4.1",
+        "fflate": "^0.6.9",
+        "potpack": "^1.0.1"
+      }
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
+    "tree-kill": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+      "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+      "dev": true
+    },
+    "troika-three-text": {
+      "version": "0.47.2",
+      "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.47.2.tgz",
+      "integrity": "sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==",
+      "requires": {
+        "bidi-js": "^1.0.2",
+        "troika-three-utils": "^0.47.2",
+        "troika-worker-utils": "^0.47.2",
+        "webgl-sdf-generator": "1.1.1"
+      }
+    },
+    "troika-three-utils": {
+      "version": "0.47.2",
+      "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.47.2.tgz",
+      "integrity": "sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg=="
+    },
+    "troika-worker-utils": {
+      "version": "0.47.2",
+      "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz",
+      "integrity": "sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA=="
+    },
+    "ts-api-utils": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+      "dev": true
+    },
+    "tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+    },
+    "type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1"
+      }
+    },
+    "type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true
+    },
+    "typescript": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+      "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+      "dev": true
+    },
+    "undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "dev": true
+    },
+    "unfetch": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+      "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
+    },
+    "update-browserslist-db": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "dev": true,
+      "requires": {
+        "escalade": "^3.1.1",
+        "picocolors": "^1.0.0"
+      }
+    },
+    "uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+          "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+          "dev": true
+        }
+      }
+    },
+    "url": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+      "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      }
+    },
+    "use-sync-external-store": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+      "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
+    },
+    "util": {
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+      "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+      "requires": {
+        "inherits": "^2.0.3",
+        "is-arguments": "^1.0.4",
+        "is-generator-function": "^1.0.7",
+        "is-typed-array": "^1.1.3",
+        "which-typed-array": "^1.1.2"
+      }
+    },
+    "utility-types": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
+      "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg=="
+    },
+    "uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
+    },
+    "vite": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
+      "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
+      "dev": true,
+      "requires": {
+        "esbuild": "^0.18.10",
+        "fsevents": "~2.3.2",
+        "postcss": "^8.4.27",
+        "rollup": "^3.27.1"
+      }
+    },
+    "webgl-constants": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+      "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+    },
+    "webgl-sdf-generator": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+      "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="
+    },
+    "webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "requires": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-typed-array": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
+      "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+      "requires": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.4",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0"
+      }
+    },
+    "wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        }
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "ws": {
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg=="
+    },
+    "xml2js": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+      "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+      "requires": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~11.0.0"
+      }
+    },
+    "xmlbuilder": {
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+    },
+    "xmlhttprequest-ssl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+      "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
+    },
+    "y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true
+    },
+    "yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
+    "yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
+    },
+    "yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "requires": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      }
+    },
+    "yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true
+    },
+    "yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true
+    },
+    "zustand": {
+      "version": "4.4.7",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
+      "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
+      "requires": {
+        "use-sync-external-store": "1.2.0"
+      }
+    }
+  }
+}
diff --git a/streaming-test-app/package.json b/streaming-test-app/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..23d920b7a5f4191485216c492b4b9a70e9a38107
--- /dev/null
+++ b/streaming-test-app/package.json
@@ -0,0 +1,53 @@
+{
+  "name": "streaming-test-app",
+  "private": true,
+  "version": "0.0.14",
+  "type": "module",
+  "scripts": {
+    "dev": "vite --host --strictPort",
+    "build": "vite build",
+    "preview": "vite preview",
+    "clean:node-modules": "rm -rf node_modules/",
+    "ts-check": "tsc --noEmit",
+    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+    "prettier-check": "cd ../ && yarn run prettier-base --check streaming-test-app",
+    "signal": "concurrently --names \"TS,LINT,PRETTIER\" -c \"bgBlack.bold,bgRed.bold,bgCyan.bold\" \"yarn run ts-check\" \"yarn run lint\" \"yarn run prettier-check\""
+  },
+  "dependencies": {
+    "@emotion/react": "11.11.1",
+    "@emotion/styled": "11.11.0",
+    "@mui/icons-material": "5.14.3",
+    "@mui/material": "5.14.5",
+    "@react-three/drei": "^9.83.9",
+    "@react-three/fiber": "^8.14.1",
+    "@react-three/xr": "^5.7.1",
+    "amazon-cognito-identity-js": "^6.3.6",
+    "audiobuffer-to-wav": "^1.0.0",
+    "aws-sdk": "^2.1472.0",
+    "js-cookie": "^3.0.5",
+    "lodash": "4.17.21",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-google-charts": "^4.0.1",
+    "socket.io-client": "^4.7.2",
+    "three": "^0.156.1",
+    "three-mesh-ui": "^6.5.4",
+    "uuid": "^9.0.0",
+    "zustand": "^4.4.3"
+  },
+  "devDependencies": {
+    "@types/node": "^20.5.3",
+    "@types/react": "^18.2.15",
+    "@types/react-dom": "^18.2.7",
+    "@types/uuid": "^9.0.2",
+    "@typescript-eslint/eslint-plugin": "^6.0.0",
+    "@typescript-eslint/parser": "^6.0.0",
+    "@vitejs/plugin-react": "^4.0.3",
+    "concurrently": "8.2.1",
+    "eslint": "^8.45.0",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "eslint-plugin-react-refresh": "^0.4.3",
+    "typescript": "5.1.6",
+    "vite": "^4.4.5"
+  }
+}
diff --git a/streaming-test-app/src/App.tsx b/streaming-test-app/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed124a1b3cea3098e0a059f5ac0cf8e00c6bd88c
--- /dev/null
+++ b/streaming-test-app/src/App.tsx
@@ -0,0 +1,57 @@
+import SocketWrapper from './SocketWrapper';
+import {ThemeProvider} from '@mui/material/styles';
+import theme from './theme';
+import StreamingInterface from './StreamingInterface';
+import CssBaseline from '@mui/material/CssBaseline';
+import {createContext, useCallback, useState} from 'react';
+import packageJson from '../package.json';
+
+console.log(`Streaming React App version: ${packageJson?.version}`);
+
+// Roboto font for mui ui library
+// import '@fontsource/roboto/300.css';
+// import '@fontsource/roboto/400.css';
+// import '@fontsource/roboto/500.css';
+// import '@fontsource/roboto/700.css';
+
+export const AppResetKeyContext = createContext<(newKey: string) => void>(
+  () => {
+    throw new Error('AppResetKeyContext not initialized');
+  },
+);
+
+function App() {
+  return (
+    
+      
+         
+     
+  );
+}
+
+function AppWrapper() {
+  const [appResetKey, setAppResetKey] = useState('[initial value]');
+  const setAppResetKeyHandler = useCallback((newKey: string) => {
+    setAppResetKey((prev) => {
+      console.warn(
+        `Resetting the app with appResetKey: ${newKey}; prevKey: ${prev}`,
+      );
+      if (prev === newKey) {
+        console.error(
+          `The appResetKey was the same as the previous key, so the app will not reset.`,
+        );
+      }
+      return newKey;
+    });
+  }, []);
+
+  return (
+    
+       
+  );
+}
+
+export default AppWrapper;
diff --git a/streaming-test-app/src/Blink.tsx b/streaming-test-app/src/Blink.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e569212f6edfa474406cb097109ea94f744448c8
--- /dev/null
+++ b/streaming-test-app/src/Blink.tsx
@@ -0,0 +1,41 @@
+import Box from '@mui/material/Box';
+import {useEffect, useState} from 'react';
+
+type Props = {
+  intervalMs: number;
+  children: React.ReactNode;
+  shouldBlink: boolean;
+  // display?: 'block' | 'inline' | 'inline-block';
+};
+
+export default function Blink({
+  // display = 'inline-block',
+  shouldBlink,
+  intervalMs,
+  children,
+}: Props): React.ReactElement {
+  const [cursorBlinkOn, setCursorBlinkOn] = useState(false);
+
+  useEffect(() => {
+    if (shouldBlink) {
+      const interval = setInterval(() => {
+        setCursorBlinkOn((prev) => !prev);
+      }, intervalMs);
+
+      return () => clearInterval(interval);
+    } else {
+      setCursorBlinkOn(false);
+    }
+  }, [intervalMs, shouldBlink]);
+
+  return (
+    
+      {children}
+     
+  );
+}
diff --git a/streaming-test-app/src/DebugSection.tsx b/streaming-test-app/src/DebugSection.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6bee96e157e84cbc04ec4f1a63968cf8e1838d62
--- /dev/null
+++ b/streaming-test-app/src/DebugSection.tsx
@@ -0,0 +1,62 @@
+import {Chart} from 'react-google-charts';
+import debug from './debug';
+import {
+  Accordion,
+  AccordionDetails,
+  AccordionSummary,
+  Button,
+  Typography,
+} from '@mui/material';
+import {useState} from 'react';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+
+export default function DebugChart() {
+  const [showDebugTimings, setShowDebugTimings] = useState(false);
+
+  const data = debug()?.getChartData();
+  const options = {
+    timeline: {
+      groupByRowLabel: true,
+    },
+  };
+
+  return (
+    
+      
 setShowDebugTimings(!showDebugTimings)}
+        elevation={0}
+        sx={{border: 1, borderColor: 'rgba(0, 0, 0, 0.3)'}}>
+        
+          {data && data.length > 1 ? (
+            <>
+               {
+                  debug()?.downloadInputAudio();
+                  debug()?.downloadOutputAudio();
+                }}>
+                Download Input / Ouput Audio
+               
+            >
+          ) : (
+            No input / output detected 
+          )}
+         
+       
+    
(
+    (roomIDParam ?? '').toUpperCase(),
+  );
+  const [roomIDError, setRoomIDError] = useState(false);
+  const [roles, setRoles] = useState<{speaker: boolean; listener: boolean}>({
+    speaker: true,
+    listener: true,
+  });
+  const [lockServer, setLockServer] = useState(false);
+  const [lockServerName, setLockServerName] = useState('');
+
+  const [joinInProgress, setJoinInProgress] = useState(false);
+  const [didAttemptAutoJoin, setDidAttemptAutoJoin] = useState(false);
+
+  const isValidServerLock =
+    lockServer === false ||
+    (lockServerName != null && lockServerName.length > 0);
+  const isValidRoles = Object.values(roles).filter(Boolean).length > 0;
+  const isValidAllInputs =
+    isValidRoomID(roomID) && isValidRoles && isValidServerLock;
+  const roomIDFromServer = roomState?.room_id ?? null;
+
+  const onJoinRoom = useCallback(
+    (createNewRoom: boolean) => {
+      if (socket == null) {
+        console.error('Socket is null, cannot join room');
+        return;
+      }
+      console.debug(`Attempting to join roomID ${roomID}...`);
+
+      const lockServerValidated: string | null =
+        lockServer && roles['speaker'] ? lockServerName : null;
+
+      setJoinInProgress(true);
+
+      const configObject: JoinRoomConfig = {
+        roles: (Object.keys(roles) as Array).filter(
+          (role) => roles[role] === true,
+        ),
+        lockServerName: lockServerValidated,
+      };
+
+      socket.emit(
+        'join_room',
+        clientID,
+        createNewRoom ? null : roomID,
+        configObject,
+        (result) => {
+          console.log('join_room result:', result);
+          if (result.message === 'max_users') {
+            setHasMaxUsers(true);
+            setJoinInProgress(false);
+            return;
+          } else {
+            setHasMaxUsers(false);
+          }
+          if (createNewRoom) {
+            setRoomID(result.roomID);
+          }
+          if (onJoinRoomOrUpdateRoles != null) {
+            onJoinRoomOrUpdateRoles();
+          }
+          setURLParam('roomID', result.roomID);
+          setJoinInProgress(false);
+        },
+      );
+    },
+    [
+      clientID,
+      lockServer,
+      lockServerName,
+      onJoinRoomOrUpdateRoles,
+      roles,
+      roomID,
+      socket,
+    ],
+  );
+
+  useEffect(() => {
+    if (
+      autoJoinRoom === true &&
+      didAttemptAutoJoin === false &&
+      socket != null
+    ) {
+      // We want to consider this an attempt whether or not we actually try to join, because
+      // we only want auto-join to happen on initial load
+      setDidAttemptAutoJoin(true);
+      if (
+        isValidAllInputs &&
+        joinInProgress === false &&
+        roomIDFromServer == null
+      ) {
+        console.debug('Attempting to auto-join room...');
+
+        onJoinRoom(false);
+      } else {
+        console.debug('Unable to auto-join room', {
+          isValidAllInputs,
+          joinInProgress,
+          roomIDFromServer,
+        });
+      }
+    }
+  }, [
+    autoJoinRoom,
+    didAttemptAutoJoin,
+    isValidAllInputs,
+    joinInProgress,
+    onJoinRoom,
+    roomIDFromServer,
+    socket,
+  ]);
+
+  return (
+    
+      
+         {
+            const id = e.target.value.toUpperCase();
+            if (isValidPartialRoomID(id)) {
+              setRoomIDError(false);
+              setRoomID(id);
+            } else {
+              setRoomIDError(true);
+            }
+          }}
+          sx={{width: '8em'}}
+        />
+
+        
+           onJoinRoom(false)}>
+            {roomState?.room_id != null ? 'Update Roles' : 'Join Room'}
+           
+        
+
+        {roomState?.room_id == null && (
+          
+             onJoinRoom(true)}>
+              {'Create New Room'}
+             
+          
+        )}
+        
+
+      
+        {Object.keys(roles).map((role) => {
+          return (
+            ) => {
+                    setRoles((prevRoles) => ({
+                      ...prevRoles,
+                      [role]: event.target.checked,
+                    }));
+                  }}
+                />
+              }
+              label={capitalize(role)}
+            />
+          );
+        })}
+
+        {urlParams.enableServerLock && roles['speaker'] === true && (
+          <>
+            ) => {
+                    setLockServer(event.target.checked);
+                  }}
+                />
+              }
+              label="Lock Server (prevent other users from streaming)"
+            />
+          >
+        )}
+         
+
+      {urlParams.enableServerLock &&
+        roles['speaker'] === true &&
+        lockServer && (
+          ) => {
+              setLockServerName(event.target.value);
+            }}
+            helperText="Locking the server will prevent anyone else from using it until you close the page, in order to maximize server performance. Please only use this for live demos."
+          />
+        )}
+
+      {serverState?.serverLock != null &&
+        serverState.serverLock.clientID === clientID && (
+          {`The server is now locked for your use (${serverState?.serverLock?.name}). Close this window to release the lock so that others may use the server.`} 
+        )}
+      
+  );
+}
diff --git a/streaming-test-app/src/SocketWrapper.tsx b/streaming-test-app/src/SocketWrapper.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e43bd142a56e06e0b50cb31ca24d7245f84fc959
--- /dev/null
+++ b/streaming-test-app/src/SocketWrapper.tsx
@@ -0,0 +1,218 @@
+import {useContext, useEffect, useMemo, useRef, useState} from 'react';
+import socketIOClient, {Socket} from 'socket.io-client';
+import useStable from './useStable';
+import {v4 as uuidv4} from 'uuid';
+import {SocketContext} from './useSocket';
+import {AppResetKeyContext} from './App';
+import Backdrop from '@mui/material/Backdrop';
+import CircularProgress from '@mui/material/CircularProgress';
+import Typography from '@mui/material/Typography';
+import {getURLParams} from './URLParams';
+
+// The time to wait before showing a "disconnected" screen upon initial app load
+const INITIAL_DISCONNECT_SCREEN_DELAY = 2000;
+const SERVER_URL_DEFAULT = `${window.location.protocol === "https:" ? "wss" : "ws"
+                    }://${window.location.host}`;
+
+export default function SocketWrapper({children}) {
+  const [socket, setSocket] = useState(null);
+  const [connected, setConnected] = useState(null);
+  // Default to true:
+  const [willAttemptReconnect] = useState(true);
+  const serverIDRef = useRef(null);
+
+  const setAppResetKey = useContext(AppResetKeyContext);
+
+  /**
+   * Previously we had stored the clientID in local storage, but in that case
+   * if a user refreshes their page they'll still have the same clientID, and
+   * will be put back into the same room, which may be confusing if they're trying
+   * to join a new room or reset the app interface. So now clientIDs persist only as
+   * long as the react app full lifecycle
+   */
+  const clientID = useStable(() => {
+    const newID = uuidv4();
+    // Set the clientID in session storage so if the page reloads the person
+    // still retains their member/room config
+    return newID;
+  });
+
+  const socketObject = useMemo(
+    () => ({socket, clientID, connected: connected ?? false}),
+    [socket, clientID, connected],
+  );
+
+  useEffect(() => {
+    const queryParams = {
+      clientID: clientID,
+    };
+
+    const serverURLFromParams = getURLParams().serverURL;
+    const serverURL = serverURLFromParams ?? SERVER_URL_DEFAULT;
+
+    console.log(
+      `Opening socket connection to ${
+        serverURL?.length === 0 ? 'window.location.host' : serverURL
+      } with query params:`,
+      queryParams,
+    );
+
+    const newSocket: Socket = socketIOClient(serverURL, {
+      query: queryParams,
+      // Normally socket.io will fallback to http polling, but we basically never
+      // want that because that'd mean awful performance. It'd be better for the app
+      // to simply break in that case and not connect.
+      transports: ['websocket'],
+      path: '/ws/socket.io'
+    });
+
+    const onServerID = (serverID: string) => {
+      console.debug('Received server ID:', serverID);
+      if (serverIDRef.current != null) {
+        if (serverIDRef.current !== serverID) {
+          console.error(
+            'Server ID changed. Resetting the app using the app key',
+          );
+          setAppResetKey(serverID);
+        }
+      }
+      serverIDRef.current = serverID;
+    };
+
+    newSocket.on('server_id', onServerID);
+
+    setSocket(newSocket);
+
+    return () => {
+      newSocket.off('server_id', onServerID);
+      console.log(
+        'Closing socket connection in the useEffect cleanup function...',
+      );
+      newSocket.disconnect();
+      setSocket(null);
+    };
+  }, [clientID, setAppResetKey]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onAny = (eventName: string, ...args) => {
+        console.debug(`[event: ${eventName}] args:`, ...args);
+      };
+
+      socket.onAny(onAny);
+
+      return () => {
+        socket.offAny(onAny);
+      };
+    }
+    return () => {};
+  }, [socket]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onConnect = (...args) => {
+        console.debug('Connected to server with args:', ...args);
+        setConnected(true);
+      };
+
+      const onConnectError = (err) => {
+        console.error(`Connection error due to ${err.message}`);
+      };
+
+      const onDisconnect = (reason) => {
+        setConnected(false);
+        console.log(`Disconnected due to ${reason}`);
+      };
+
+      socket.on('connect', onConnect);
+      socket.on('connect_error', onConnectError);
+      socket.on('disconnect', onDisconnect);
+
+      return () => {
+        socket.off('connect', onConnect);
+        socket.off('connect_error', onConnectError);
+        socket.off('disconnect', onDisconnect);
+      };
+    }
+  }, [socket]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onReconnectError = (err) => {
+        console.log(`Reconnect error due to ${err.message}`);
+      };
+
+      socket.io.on('reconnect_error', onReconnectError);
+
+      const onError = (err) => {
+        console.log(`General socket error with message ${err.message}`);
+      };
+      socket.io.on('error', onError);
+
+      const onReconnect = (attempt) => {
+        console.log(`Reconnected after ${attempt} attempt(s)`);
+      };
+      socket.io.on('reconnect', onReconnect);
+
+      const disconnectOnBeforeUnload = () => {
+        console.log('Disconnecting due to beforeunload event...');
+        socket.disconnect();
+        setSocket(null);
+      };
+      window.addEventListener('beforeunload', disconnectOnBeforeUnload);
+
+      return () => {
+        socket.io.off('reconnect_error', onReconnectError);
+        socket.io.off('error', onError);
+        socket.io.off('reconnect', onReconnect);
+        window.removeEventListener('beforeunload', disconnectOnBeforeUnload);
+      };
+    }
+  }, [clientID, setAppResetKey, socket]);
+
+  /**
+   * Wait to show the disconnected screen on initial app load
+   */
+  useEffect(() => {
+    window.setTimeout(() => {
+      setConnected((prev) => {
+        if (prev === null) {
+          return false;
+        }
+        return prev;
+      });
+    }, INITIAL_DISCONNECT_SCREEN_DELAY);
+  }, []);
+
+  return (
+    
+      {children}
+
+       theme.zIndex.drawer + 1,
+        }}>
+        
+          
+            {'Disconnected. Attempting to reconnect...'}
+           
+        
+       
+     
+  );
+}
diff --git a/streaming-test-app/src/StreamingInterface.css b/streaming-test-app/src/StreamingInterface.css
new file mode 100644
index 0000000000000000000000000000000000000000..3ef953f83853d9cadfd700ad55b85b3bcd3b5403
--- /dev/null
+++ b/streaming-test-app/src/StreamingInterface.css
@@ -0,0 +1,56 @@
+.app-wrapper-sra {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.main-container-sra {
+  background-color: white;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  text-align: left;
+  margin: 16px;
+  margin-bottom: 36px;
+  border-radius: 8px;
+  box-shadow: 0px 24px 30px rgba(0, 0, 0, 0.3);
+  border: 1px solid rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+}
+
+.top-section-sra {
+  padding-top: 24px;
+  margin-bottom: 24px;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+}
+
+.horizontal-padding-sra {
+  padding-left: 20px;
+  padding-right: 20px;
+}
+
+.header-container-sra {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  margin-bottom: 24px;
+}
+
+.header-icon-sra {
+  display: block;
+  margin-right: 12px;
+}
+
+.translation-text-container-sra {
+  background-color: #f8f8f8;
+  padding-top: 12px;
+  padding-bottom: 4px;
+}
+
+.text-chunk-sra {
+  margin-bottom: 12px;
+}
diff --git a/streaming-test-app/src/StreamingInterface.tsx b/streaming-test-app/src/StreamingInterface.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..84450b47f808f2c9a74fd7868895e1f2bb1533eb
--- /dev/null
+++ b/streaming-test-app/src/StreamingInterface.tsx
@@ -0,0 +1,1219 @@
+import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+import InputLabel from '@mui/material/InputLabel';
+import FormControl from '@mui/material/FormControl';
+import Select, {SelectChangeEvent} from '@mui/material/Select';
+import MenuItem from '@mui/material/MenuItem';
+import Stack from '@mui/material/Stack';
+import seamlessLogoUrl from './assets/seamless.svg';
+import {
+  AgentCapabilities,
+  BaseResponse,
+  BrowserAudioStreamConfig,
+  DynamicConfig,
+  PartialDynamicConfig,
+  SUPPORTED_INPUT_SOURCES,
+  SUPPORTED_OUTPUT_MODES,
+  ServerExceptionData,
+  ServerSpeechData,
+  ServerState,
+  ServerTextData,
+  StartStreamEventConfig,
+  StreamingStatus,
+  SupportedInputSource,
+  SupportedOutputMode,
+  TranslationSentences,
+} from './types/StreamingTypes';
+import FormLabel from '@mui/material/FormLabel';
+import RadioGroup from '@mui/material/RadioGroup';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Radio from '@mui/material/Radio';
+import './StreamingInterface.css';
+import RoomConfig from './RoomConfig';
+import Divider from '@mui/material/Divider';
+import {useSocket} from './useSocket';
+import {RoomState} from './types/RoomState';
+import useStable from './useStable';
+import float32To16BitPCM from './float32To16BitPCM';
+import createBufferedSpeechPlayer from './createBufferedSpeechPlayer';
+import Checkbox from '@mui/material/Checkbox';
+import Alert from '@mui/material/Alert';
+import isScrolledToDocumentBottom from './isScrolledToDocumentBottom';
+import Box from '@mui/material/Box';
+import Slider from '@mui/material/Slider';
+import VolumeDown from '@mui/icons-material/VolumeDown';
+import VolumeUp from '@mui/icons-material/VolumeUp';
+import Mic from '@mui/icons-material/Mic';
+import MicOff from '@mui/icons-material/MicOff';
+import XRDialog from './react-xr/XRDialog';
+import getTranslationSentencesFromReceivedData from './getTranslationSentencesFromReceivedData';
+import {
+  sliceTranslationSentencesUpToIndex,
+  getTotalSentencesLength,
+} from './sliceTranslationSentencesUtils';
+import Blink from './Blink';
+import {CURSOR_BLINK_INTERVAL_MS} from './cursorBlinkInterval';
+import {getURLParams} from './URLParams';
+import debug from './debug';
+import DebugSection from './DebugSection';
+import Switch from '@mui/material/Switch';
+import Grid from '@mui/material/Grid';
+import {getLanguageFromThreeLetterCode} from './languageLookup';
+import HeadphonesIcon from '@mui/icons-material/Headphones';
+
+const AUDIO_STREAM_DEFAULTS = {
+  userMedia: {
+    echoCancellation: false,
+    noiseSuppression: true,
+  },
+  displayMedia: {
+    echoCancellation: false,
+    noiseSuppression: false,
+  },
+} as const;
+
+async function requestUserMediaAudioStream(
+  config: BrowserAudioStreamConfig = AUDIO_STREAM_DEFAULTS['userMedia'],
+) {
+  const stream = await navigator.mediaDevices.getUserMedia({
+    audio: {...config, channelCount: 1},
+  });
+  console.debug(
+    '[requestUserMediaAudioStream] stream created with settings:',
+    stream.getAudioTracks()?.[0]?.getSettings(),
+  );
+  return stream;
+}
+
+async function requestDisplayMediaAudioStream(
+  config: BrowserAudioStreamConfig = AUDIO_STREAM_DEFAULTS['displayMedia'],
+) {
+  const stream = await navigator.mediaDevices.getDisplayMedia({
+    audio: {...config, channelCount: 1},
+  });
+  console.debug(
+    '[requestDisplayMediaAudioStream] stream created with settings:',
+    stream.getAudioTracks()?.[0]?.getSettings(),
+  );
+  return stream;
+}
+
+const buttonLabelMap: {[key in StreamingStatus]: string} = {
+  stopped: 'Start Streaming',
+  running: 'Stop Streaming',
+  starting: 'Starting...',
+};
+
+const BUFFER_LIMIT = 1;
+
+const SCROLLED_TO_BOTTOM_THRESHOLD_PX = 36;
+
+const GAIN_MULTIPLIER_OVER_1 = 3;
+
+const getGainScaledValue = (value) =>
+  value > 1 ? (value - 1) * GAIN_MULTIPLIER_OVER_1 + 1 : value;
+
+const TOTAL_ACTIVE_TRANSCODER_WARNING_THRESHOLD = 2;
+
+const MAX_SERVER_EXCEPTIONS_TRACKED = 500;
+
+export const TYPING_ANIMATION_DELAY_MS = 6;
+
+export default function StreamingInterface() {
+  const urlParams = getURLParams();
+  const debugParam = urlParams.debug;
+  const [animateTextDisplay, setAnimateTextDisplay] = useState(
+    urlParams.animateTextDisplay,
+  );
+
+  const socketObject = useSocket();
+  const {socket, clientID} = socketObject;
+
+  const [serverState, setServerState] = useState(null);
+  const [agent, setAgent] = useState(null);
+  const model = agent?.name ?? null;
+  const agentsCapabilities: Array =
+    serverState?.agentsCapabilities ?? [];
+  const currentAgent: AgentCapabilities | null =
+    agentsCapabilities.find((agent) => agent.name === model) ?? null;
+
+  const [serverExceptions, setServerExceptions] = useState<
+    Array
+  >([]);
+  const [roomState, setRoomState] = useState(null);
+  const roomID = roomState?.room_id ?? null;
+  const isSpeaker =
+    (clientID != null && roomState?.speakers.includes(clientID)) ?? false;
+  const isListener =
+    (clientID != null && roomState?.listeners.includes(clientID)) ?? false;
+
+  const [streamingStatus, setStreamingStatus] =
+    useState('stopped');
+
+  const isStreamConfiguredRef = useRef(false);
+  const [hasMaxUsers, setHasMaxUsers] = useState(false);
+
+  const [outputMode, setOutputMode] = useState('s2s&t');
+  const [inputSource, setInputSource] =
+    useState('userMedia');
+  const [enableNoiseSuppression, setEnableNoiseSuppression] = useState<
+    boolean | null
+  >(null);
+  const [enableEchoCancellation, setEnableEchoCancellation] = useState<
+    boolean | null
+  >(null);
+
+  // Dynamic Params:
+  const [targetLang, setTargetLang] = useState(null);
+  const [enableExpressive, setEnableExpressive] = useState(
+    null,
+  );
+
+  const [serverDebugFlag, setServerDebugFlag] = useState(
+    debugParam ?? false,
+  );
+
+  const [receivedData, setReceivedData] = useState>([]);
+  const [
+    translationSentencesAnimatedIndex,
+    setTranslationSentencesAnimatedIndex,
+  ] = useState(0);
+
+  const lastTranslationResultRef = useRef(null);
+
+  const [inputStream, setInputStream] = useState(null);
+  const [inputStreamSource, setInputStreamSource] =
+    useState(null);
+  const audioContext = useStable(() => new AudioContext());
+  const [scriptNodeProcessor, setScriptNodeProcessor] =
+    useState(null);
+
+  const [muted, setMuted] = useState(false);
+  // The onaudioprocess script needs an up-to-date reference to the muted state, so
+  // we use a ref here and keep it in sync via useEffect
+  const mutedRef = useRef(muted);
+  useEffect(() => {
+    mutedRef.current = muted;
+  }, [muted]);
+
+  const [gain, setGain] = useState(1);
+
+  const isScrolledToBottomRef = useRef(isScrolledToDocumentBottom());
+
+  // Some config options must be set when starting streaming and cannot be chaned dynamically.
+  // This controls whether they are disabled or not
+  const streamFixedConfigOptionsDisabled =
+    streamingStatus !== 'stopped' || roomID == null;
+
+  const bufferedSpeechPlayer = useStable(() => {
+    const player = createBufferedSpeechPlayer({
+      onStarted: () => {
+        console.debug('📢 PLAYBACK STARTED 📢');
+      },
+      onEnded: () => {
+        console.debug('🛑 PLAYBACK ENDED 🛑');
+      },
+    });
+
+    // Start the player now so it eagerly plays audio when it arrives
+    player.start();
+    return player;
+  });
+
+  const translationSentencesBase: TranslationSentences =
+    getTranslationSentencesFromReceivedData(receivedData);
+
+  const translationSentencesBaseTotalLength = getTotalSentencesLength(
+    translationSentencesBase,
+  );
+
+  const translationSentences: TranslationSentences = animateTextDisplay
+    ? sliceTranslationSentencesUpToIndex(
+        translationSentencesBase,
+        translationSentencesAnimatedIndex,
+      )
+    : translationSentencesBase;
+
+  // We want the blinking cursor to show before any text has arrived, so let's add an empty string so that the cursor shows up
+  const translationSentencesWithEmptyStartingString =
+    streamingStatus === 'running' && translationSentences.length === 0
+      ? ['']
+      : translationSentences;
+
+  /******************************************
+   * Event Handlers
+   ******************************************/
+
+  const setAgentAndUpdateParams = useCallback(
+    (newAgent: AgentCapabilities | null) => {
+      setAgent((prevAgent) => {
+        if (prevAgent?.name !== newAgent?.name) {
+          setTargetLang(newAgent?.targetLangs[0] ?? null);
+          setEnableExpressive(null);
+        }
+        return newAgent;
+      });
+    },
+    [],
+  );
+
+  const onSetDynamicConfig = useCallback(
+    async (partialConfig: PartialDynamicConfig) => {
+      return new Promise((resolve, reject) => {
+        if (socket == null) {
+          reject(new Error('[onSetDynamicConfig] socket is null '));
+          return;
+        }
+
+        socket.emit(
+          'set_dynamic_config',
+          partialConfig,
+          (result: BaseResponse) => {
+            console.log('[emit result: set_dynamic_config]', result);
+            if (result.status === 'ok') {
+              resolve();
+            } else {
+              reject();
+            }
+          },
+        );
+      });
+    },
+    [socket],
+  );
+
+  const configureStreamAsync = ({sampleRate}: {sampleRate: number}) => {
+    return new Promise((resolve, reject) => {
+      if (socket == null) {
+        reject(new Error('[configureStreamAsync] socket is null '));
+        return;
+      }
+      const modelName = agent?.name ?? null;
+      if (modelName == null) {
+        reject(new Error('[configureStreamAsync] modelName is null '));
+        return;
+      }
+
+      const config: StartStreamEventConfig = {
+        event: 'config',
+        rate: sampleRate,
+        model_name: modelName,
+        debug: serverDebugFlag,
+        // synchronous processing isn't implemented on the v2 pubsub server, so hardcode this to true
+        async_processing: true,
+        buffer_limit: BUFFER_LIMIT,
+        model_type: outputMode,
+      };
+
+      console.log('[configureStreamAsync] sending config', config);
+
+      socket.emit('configure_stream', config, (statusObject) => {
+        if (statusObject.status === 'ok') {
+          isStreamConfiguredRef.current = true;
+          console.debug(
+            '[configureStreamAsync] stream configured!',
+            statusObject,
+          );
+          resolve();
+        } else {
+          isStreamConfiguredRef.current = false;
+          reject(
+            new Error(
+              `[configureStreamAsync] configure_stream returned status: ${statusObject.status}`,
+            ),
+          );
+          return;
+        }
+      });
+    });
+  };
+
+  const startStreaming = async () => {
+    if (streamingStatus !== 'stopped') {
+      console.warn(
+        `Attempting to start stream when status is ${streamingStatus}`,
+      );
+      return;
+    }
+
+    setStreamingStatus('starting');
+
+    if (audioContext.state === 'suspended') {
+      console.warn('audioContext was suspended! resuming...');
+      await audioContext.resume();
+    }
+
+    let stream: MediaStream | null = null;
+
+    try {
+      if (inputSource === 'userMedia') {
+        stream = await requestUserMediaAudioStream({
+          noiseSuppression:
+            enableNoiseSuppression ??
+            AUDIO_STREAM_DEFAULTS['userMedia'].noiseSuppression,
+          echoCancellation:
+            enableEchoCancellation ??
+            AUDIO_STREAM_DEFAULTS['userMedia'].echoCancellation,
+        });
+      } else if (inputSource === 'displayMedia') {
+        stream = await requestDisplayMediaAudioStream({
+          noiseSuppression:
+            enableNoiseSuppression ??
+            AUDIO_STREAM_DEFAULTS['displayMedia'].noiseSuppression,
+          echoCancellation:
+            enableEchoCancellation ??
+            AUDIO_STREAM_DEFAULTS['displayMedia'].echoCancellation,
+        });
+      } else {
+        throw new Error(`Unsupported input source requested: ${inputSource}`);
+      }
+      setInputStream(stream);
+    } catch (e) {
+      console.error('[startStreaming] media stream request failed:', e);
+      setStreamingStatus('stopped');
+      return;
+    }
+
+    const mediaStreamSource = audioContext.createMediaStreamSource(stream);
+    setInputStreamSource(mediaStreamSource);
+    /**
+     * NOTE: This currently uses a deprecated way of processing the audio (createScriptProcessor), but
+     * which is easy and convenient for our purposes.
+     *
+     * Documentation for the deprecated way of doing it is here: https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createScriptProcessor
+     *
+     * In an ideal world this would be migrated to something like this SO answer: https://stackoverflow.com/a/65448287
+     */
+    const scriptProcessor = audioContext.createScriptProcessor(16384, 1, 1);
+    setScriptNodeProcessor(scriptProcessor);
+
+    scriptProcessor.onaudioprocess = (event) => {
+      if (isStreamConfiguredRef.current === false) {
+        console.debug('[onaudioprocess] stream is not configured yet!');
+        return;
+      }
+      if (socket == null) {
+        console.warn('[onaudioprocess] socket is null in onaudioprocess');
+        return;
+      }
+
+      if (mutedRef.current) {
+        // We still want to send audio to the server when we're muted to ensure we
+        // get any remaining audio back from the server, so let's pass an array length 1 with a value of 0
+        const mostlyEmptyInt16Array = new Int16Array(1);
+        socket.emit('incoming_audio', mostlyEmptyInt16Array);
+      } else {
+        const float32Audio = event.inputBuffer.getChannelData(0);
+        const pcm16Audio = float32To16BitPCM(float32Audio);
+        socket.emit('incoming_audio', pcm16Audio);
+      }
+
+      debug()?.sentAudio(event);
+    };
+
+    mediaStreamSource.connect(scriptProcessor);
+    scriptProcessor.connect(audioContext.destination);
+
+    bufferedSpeechPlayer.start();
+
+    try {
+      if (targetLang == null) {
+        throw new Error('[startStreaming] targetLang cannot be nullish');
+      }
+
+      // When we are starting the stream we want to pass all the dynamic config values
+      // available before actually configuring and starting the stream
+      const fullDynamicConfig: DynamicConfig = {
+        targetLanguage: targetLang,
+        expressive: enableExpressive,
+      };
+
+      await onSetDynamicConfig(fullDynamicConfig);
+
+      // NOTE: this needs to be the *audioContext* sample rate, not the sample rate of the input stream. Not entirely sure why.
+      await configureStreamAsync({
+        sampleRate: audioContext.sampleRate,
+      });
+    } catch (e) {
+      console.error('configureStreamAsync failed', e);
+      setStreamingStatus('stopped');
+      return;
+    }
+
+    setStreamingStatus('running');
+  };
+
+  const stopStreaming = useCallback(async () => {
+    if (streamingStatus === 'stopped') {
+      console.warn(
+        `Attempting to stop stream when status is ${streamingStatus}`,
+      );
+      return;
+    }
+
+    // Stop the speech playback right away
+    bufferedSpeechPlayer.stop();
+
+    if (inputStreamSource == null || scriptNodeProcessor == null) {
+      console.error(
+        'inputStreamSource || scriptNodeProcessor is null in stopStreaming',
+      );
+    } else {
+      inputStreamSource.disconnect(scriptNodeProcessor);
+      scriptNodeProcessor.disconnect(audioContext.destination);
+
+      // Release the mic input so we stop showing the red recording icon in the browser
+      inputStream?.getTracks().forEach((track) => track.stop());
+    }
+
+    if (socket == null) {
+      console.warn('Unable to emit stop_stream because socket is null');
+    } else {
+      socket.emit('stop_stream', (result) => {
+        console.debug('[emit result: stop_stream]', result);
+      });
+    }
+
+    setStreamingStatus('stopped');
+  }, [
+    audioContext.destination,
+    bufferedSpeechPlayer,
+    inputStream,
+    inputStreamSource,
+    scriptNodeProcessor,
+    socket,
+    streamingStatus,
+  ]);
+
+  const onClearTranscriptForAll = useCallback(() => {
+    if (socket != null) {
+      socket.emit('clear_transcript_for_all');
+    }
+  }, [socket]);
+
+  /******************************************
+   * Effects
+   ******************************************/
+
+  useEffect(() => {
+    if (socket == null) {
+      return;
+    }
+
+    const onRoomStateUpdate = (roomState: RoomState) => {
+      setRoomState(roomState);
+    };
+
+    socket.on('room_state_update', onRoomStateUpdate);
+
+    return () => {
+      socket.off('room_state_update', onRoomStateUpdate);
+    };
+  }, [socket]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onTranslationText = (data: ServerTextData) => {
+        setReceivedData((prev) => [...prev, data]);
+        debug()?.receivedText(data.payload);
+      };
+
+      const onTranslationSpeech = (data: ServerSpeechData) => {
+        bufferedSpeechPlayer.addAudioToBuffer(data.payload, data.sample_rate);
+      };
+
+      socket.on('translation_text', onTranslationText);
+      socket.on('translation_speech', onTranslationSpeech);
+
+      return () => {
+        socket.off('translation_text', onTranslationText);
+        socket.off('translation_speech', onTranslationSpeech);
+      };
+    }
+  }, [bufferedSpeechPlayer, socket]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onServerStateUpdate = (newServerState: ServerState) => {
+        setServerState(newServerState);
+
+        // If a client creates a server lock, we want to stop streaming if we're not them
+        if (
+          newServerState.serverLock?.isActive === true &&
+          newServerState.serverLock?.clientID !== clientID &&
+          streamingStatus === 'running'
+        ) {
+          stopStreaming();
+        }
+
+        const firstAgentNullable = newServerState.agentsCapabilities[0];
+        if (agent == null && firstAgentNullable != null) {
+          setAgentAndUpdateParams(firstAgentNullable);
+        }
+      };
+
+      socket.on('server_state_update', onServerStateUpdate);
+
+      return () => {
+        socket.off('server_state_update', onServerStateUpdate);
+      };
+    }
+  }, [
+    agent,
+    clientID,
+    setAgentAndUpdateParams,
+    socket,
+    stopStreaming,
+    streamingStatus,
+  ]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onServerException = (
+        exceptionDataWithoutClientTime: ServerExceptionData,
+      ) => {
+        const exceptionData = {
+          ...exceptionDataWithoutClientTime,
+          timeStringClient: new Date(
+            exceptionDataWithoutClientTime['timeEpochMs'],
+          ).toLocaleString(),
+        };
+
+        setServerExceptions((prev) =>
+          [exceptionData, ...prev].slice(0, MAX_SERVER_EXCEPTIONS_TRACKED),
+        );
+        console.error(
+          `[server_exception] The server encountered an exception: ${exceptionData['message']}`,
+          exceptionData,
+        );
+      };
+
+      socket.on('server_exception', onServerException);
+
+      return () => {
+        socket.off('server_exception', onServerException);
+      };
+    }
+  }, [socket]);
+
+  useEffect(() => {
+    if (socket != null) {
+      const onClearTranscript = () => {
+        setReceivedData([]);
+        setTranslationSentencesAnimatedIndex(0);
+      };
+
+      socket.on('clear_transcript', onClearTranscript);
+
+      return () => {
+        socket.off('clear_transcript', onClearTranscript);
+      };
+    }
+  }, [socket]);
+
+  useEffect(() => {
+    const onScroll = () => {
+      if (isScrolledToDocumentBottom(SCROLLED_TO_BOTTOM_THRESHOLD_PX)) {
+        isScrolledToBottomRef.current = true;
+        return;
+      }
+      isScrolledToBottomRef.current = false;
+      return;
+    };
+
+    document.addEventListener('scroll', onScroll);
+
+    return () => {
+      document.removeEventListener('scroll', onScroll);
+    };
+  }, []);
+
+  useLayoutEffect(() => {
+    if (
+      lastTranslationResultRef.current != null &&
+      isScrolledToBottomRef.current
+    ) {
+      // Scroll the div to the most recent entry
+      lastTranslationResultRef.current.scrollIntoView();
+    }
+    // Run the effect every time data is received, so that
+    // we scroll to the bottom even if we're just adding text to
+    // a pre-existing chunk
+  }, [receivedData]);
+
+  useEffect(() => {
+    if (!animateTextDisplay) {
+      return;
+    }
+
+    if (
+      translationSentencesAnimatedIndex < translationSentencesBaseTotalLength
+    ) {
+      const timeout = setTimeout(() => {
+        setTranslationSentencesAnimatedIndex((prev) => prev + 1);
+        debug()?.startRenderText();
+      }, TYPING_ANIMATION_DELAY_MS);
+
+      return () => clearTimeout(timeout);
+    } else {
+      debug()?.endRenderText();
+    }
+  }, [
+    animateTextDisplay,
+    translationSentencesAnimatedIndex,
+    translationSentencesBaseTotalLength,
+  ]);
+
+  /******************************************
+   * Sub-components
+   ******************************************/
+
+  const volumeSliderNode = (
+    
+       `${(value * 100).toFixed(0)}%`}
+        valueLabelDisplay="auto"
+        value={gain}
+        onChange={(_event: Event, newValue: number | number[]) => {
+          if (typeof newValue === 'number') {
+            const scaledGain = getGainScaledValue(newValue);
+            // We want the actual gain node to use the scaled value
+            bufferedSpeechPlayer.setGain(scaledGain);
+            // But we want react state to keep track of the non-scaled value
+            setGain(newValue);
+          } else {
+            console.error(
+              `[volume slider] Unexpected non-number value: ${newValue}`,
+            );
+          }
+        }}
+      />
+        
+  );
+
+  const xrDialogComponent = (
+     {
+        setAnimateTextDisplay(urlParams.animateTextDisplay);
+      }}
+      onARVisible={() => setAnimateTextDisplay(false)}
+    />
+  );
+
+  return (
+    
+      
+        
+          
+            
+              
+
+              
+                
+                  Seamless Translation
+                 
+              
+            
+            
+              
+                
+                  Welcome! This space is limited to one user at a time.
+                  If using the live HF space, sharing room code to listeners on another
+                  IP address may not work because it's running on different replicas.
+                  Use headphones if you are both speaker and listener to prevent feedback.
+                  here .
+                  In your duplicated space, join a room as speaker or listener (or both),
+                  and share the room code to invite listeners.
+                  README  for more information.
+                   
+              
+            
+            
+              
+                 {
+                    // If the user has switched from speaker to listener we need to tell the
+                    // player to play eagerly, since currently the listener doesn't have any stop/start controls
+                    bufferedSpeechPlayer.start();
+                  }}
+                />
+
+                {isListener && !isSpeaker && (
+                  
+                    {volumeSliderNode}
+                   
+                )}
+                
+
+              {isSpeaker && (
+                <>
+                  
+                    
+                      Model
+                     
+                    
+                      
+                        Model
+                       
+                       {
+                          const newAgent =
+                            agentsCapabilities.find(
+                              (agent) => e.target.value === agent.name,
+                            ) ?? null;
+                          if (newAgent == null) {
+                            console.error(
+                              'Unable to find agent with name',
+                              e.target.value,
+                            );
+                          }
+                          setAgentAndUpdateParams(newAgent);
+                        }}
+                        value={model ?? ''}>
+                        {agentsCapabilities.map((agent) => (
+                          
+                            {agent.name}
+                           
+                        ))}
+                       
+                     
+
+                   
+
+                  
+                    
+                      Output
+                     
+
+                    
+                      
+                        
+                          Target Language
+                         
+                         {
+                            setTargetLang(e.target.value);
+                            onSetDynamicConfig({
+                              targetLanguage: e.target.value,
+                            });
+                          }}
+                          value={targetLang ?? ''}>
+                          {currentAgent?.targetLangs.map((langCode) => (
+                            
+                              {getLanguageFromThreeLetterCode(langCode) != null
+                                ? `${getLanguageFromThreeLetterCode(
+                                    langCode,
+                                  )} (${langCode})`
+                                : langCode}
+                             
+                          ))}
+                         
+                       
+                     
+
+                    
+                      
+                        
+                          
+                              setOutputMode(
+                                e.target.value as SupportedOutputMode,
+                              )
+                            }
+                            name="output-modes-radio-buttons-group">
+                            {
+                              // TODO: Use supported modalities from agentCapabilities
+                              SUPPORTED_OUTPUT_MODES.map(({value, label}) => (
+                                 
+                         
+                       
+
+                      
+                        
+                          {currentAgent?.dynamicParams?.includes(
+                            'expressive',
+                          ) && (
+                            ,
+                                  ) => {
+                                    const newValue = event.target.checked;
+                                    setEnableExpressive(newValue);
+                                    onSetDynamicConfig({
+                                      expressive: newValue,
+                                    });
+                                  }}
+                                />
+                              }
+                              label="Expressive"
+                            />
+                          )}
+
+                          {isListener && (
+                            
+                              {volumeSliderNode}
+                             
+                          )}
+                          
+                       
+                     
+                   
+
+                  
+                    
+                      
+                        
+                          Input Source
+                         
+                        ) =>
+                            setInputSource(
+                              e.target.value as SupportedInputSource,
+                            )
+                          }
+                          name="input-source-radio-buttons-group">
+                          {SUPPORTED_INPUT_SOURCES.map(({label, value}) => (
+                             
+                       
+                     
+
+                    
+                    
+                        Options 
+                        ,
+                              ) =>
+                                setEnableNoiseSuppression(event.target.checked)
+                              }
+                            />
+                          }
+                          label="Noise Suppression"
+                        />
+                        ,
+                              ) =>
+                                setEnableEchoCancellation(event.target.checked)
+                              }
+                            />
+                          }
+                          label="Echo Cancellation (not recommended)"
+                        />
+                        ,
+                              ) => setServerDebugFlag(event.target.checked)}
+                            />
+                          }
+                          label="Enable Server Debugging"
+                        />
+                          
+                     
+                   
+
+                  {isSpeaker &&
+                    isListener &&
+                    inputSource === 'userMedia' &&
+                    !enableEchoCancellation &&
+                    gain !== 0 && (
+                      
+                        
}>
+                          Headphones required to prevent feedback.
+                        
+                      
+                      
+                        We don't recommend using echo cancellation as it may
+                        distort the input audio. If possible, use headphones and
+                        disable echo cancellation instead.
+                       
+                    
+                    {streamingStatus === 'stopped' ? (
+                      
+                        {buttonLabelMap[streamingStatus]}
+                       
+                    ) : (
+                      
+                        {buttonLabelMap[streamingStatus]}
+                       
+                    )}
+
+                    
+                       setMuted((prev) => !prev)}
+                        sx={{
+                          borderRadius: 100,
+                          paddingX: 0,
+                          minWidth: '36px',
+                        }}>
+                        {muted ?  
+                     
+
+                    {roomID == null ? null : (
+                      
+                        {xrDialogComponent}
+                       
+                    )}
+                   
+
+                  {serverExceptions.length > 0 && (
+                    
+                      
+                        {`The server encountered an exception. See the browser console for details. You may need to refresh the page to continue using the app.`}
+                       
+                    
+                        
+                          {`The server currently has ${serverState?.totalActiveTranscoders} active streaming sessions. Performance may be degraded.`}
+                         
+                      
+                        
+                          {`The server is currently locked. Priority will be given to that client when they are streaming, and your streaming session may be halted abruptly.`}
+                         
+                      
 
+
+            {isListener && !isSpeaker && (
+              
+                {xrDialogComponent}
+               
+            )}
+          
+
+          {hasMaxUsers && (
+            
+              
+                {`Maximum number of users reached. Please try again at a later time.`}
+               
+            
+          )}
+          {debugParam && roomID != null && 
}
+
+          
+            
+              
+                Transcript
+               
+              {isSpeaker && (
+                
+                  Clear Transcript for All
+                 
+              )}
+             
+            
+              
+                {translationSentencesWithEmptyStartingString.map(
+                  (sentence, index, arr) => {
+                    const isLast = index === arr.length - 1;
+                    const maybeRef = isLast
+                      ? {ref: lastTranslationResultRef}
+                      : {};
+                    return (
+                      
+                        
+                          {sentence}
+                          {animateTextDisplay && isLast && (
+                             0
+                              }>
+                              
+                                {'|'}
+                               
+                             
+                          )}
+                         
+                      
+                    );
+                  },
+                )}
+              
 
+          
+        
 
+    
+ 
diff --git a/streaming-test-app/src/createBufferedSpeechPlayer.ts b/streaming-test-app/src/createBufferedSpeechPlayer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5ef2c5bab95190d485c6ffb34506c7862014778d
--- /dev/null
+++ b/streaming-test-app/src/createBufferedSpeechPlayer.ts
@@ -0,0 +1,173 @@
+import debug from './debug';
+
+type AddAudioToBufferFunction = (
+  samples: Array,
+  sampleRate: number,
+) => void;
+
+export type BufferedSpeechPlayer = {
+  addAudioToBuffer: AddAudioToBufferFunction;
+  setGain: (gain: number) => void;
+  start: () => void;
+  stop: () => void;
+};
+
+type Options = {
+  onEnded?: () => void;
+  onStarted?: () => void;
+};
+
+export default function createBufferedSpeechPlayer({
+  onStarted,
+  onEnded,
+}: Options): BufferedSpeechPlayer {
+  const audioContext = new AudioContext();
+  const gainNode = audioContext.createGain();
+  gainNode.connect(audioContext.destination);
+
+  let unplayedAudioBuffers: Array = [];
+
+  let currentPlayingBufferSource: AudioBufferSourceNode | null = null;
+
+  let isPlaying = false;
+
+  // This means that the player starts in the 'stopped' state, and you need to call player.start() for it to start playing
+  let shouldPlayWhenAudioAvailable = false;
+
+  const setGain = (gain: number) => {
+    gainNode.gain.setValueAtTime(gain, audioContext.currentTime);
+  };
+
+  const start = () => {
+    shouldPlayWhenAudioAvailable = true;
+    debug()?.start();
+    playNextBufferIfNotAlreadyPlaying();
+  };
+
+  // Stop will stop the audio and clear the buffers
+  const stop = () => {
+    shouldPlayWhenAudioAvailable = false;
+
+    // Stop the current buffers
+    currentPlayingBufferSource?.stop();
+    currentPlayingBufferSource = null;
+
+    unplayedAudioBuffers = [];
+
+    onEnded != null && onEnded();
+    isPlaying = false;
+    return;
+  };
+
+  const playNextBufferIfNotAlreadyPlaying = () => {
+    if (!isPlaying) {
+      playNextBuffer();
+    }
+  };
+
+  const playNextBuffer = () => {
+    if (shouldPlayWhenAudioAvailable === false) {
+      console.debug(
+        '[BufferedSpeechPlayer][playNextBuffer] Not playing any more audio because shouldPlayWhenAudioAvailable is false.',
+      );
+      // NOTE: we do not need to set isPlaying = false or call onEnded because that will be handled in the stop() function
+      return;
+    }
+    if (unplayedAudioBuffers.length === 0) {
+      console.debug(
+        '[BufferedSpeechPlayer][playNextBuffer] No buffers to play.',
+      );
+      if (isPlaying) {
+        isPlaying = false;
+        onEnded != null && onEnded();
+      }
+      return;
+    }
+
+    // If isPlaying is false, then we are starting playback fresh rather than continuing it, and should call onStarted
+    if (isPlaying === false) {
+      isPlaying = true;
+      onStarted != null && onStarted();
+    }
+
+    const source = audioContext.createBufferSource();
+
+    // Get the first unplayed buffer from the array, and remove it from the array
+    const buffer = unplayedAudioBuffers.shift() ?? null;
+    source.buffer = buffer;
+    console.debug(
+      `[BufferedSpeechPlayer] Playing buffer with ${source.buffer?.length} samples`,
+    );
+
+    source.connect(gainNode);
+
+    const startTime = new Date().getTime();
+    source.start();
+    currentPlayingBufferSource = source;
+    // This is probably not necessary, but it doesn't hurt
+    isPlaying = true;
+
+    // TODO: consider changing this to a while loop to avoid deep recursion
+    const onThisBufferPlaybackEnded = () => {
+      console.debug(
+        `[BufferedSpeechPlayer] Buffer with ${source.buffer?.length} samples ended.`,
+      );
+      source.removeEventListener('ended', onThisBufferPlaybackEnded);
+      const endTime = new Date().getTime();
+      debug()?.playedAudio(startTime, endTime, buffer);
+      currentPlayingBufferSource = null;
+
+      // We don't set isPlaying = false here because we are attempting to continue playing. It will get set to false if there are no more buffers to play
+      playNextBuffer();
+    };
+
+    source.addEventListener('ended', onThisBufferPlaybackEnded);
+  };
+
+  const addAudioToBuffer: AddAudioToBufferFunction = (samples, sampleRate) => {
+    const incomingArrayBufferChunk = audioContext.createBuffer(
+      // 1 channel
+      1,
+      samples.length,
+      sampleRate,
+    );
+
+    incomingArrayBufferChunk.copyToChannel(
+      new Float32Array(samples),
+      // first channel
+      0,
+    );
+
+    console.debug(
+      `[addAudioToBufferAndPlay] Adding buffer with ${incomingArrayBufferChunk.length} samples to queue.`,
+    );
+
+    unplayedAudioBuffers.push(incomingArrayBufferChunk);
+    debug()?.receivedAudio(
+      incomingArrayBufferChunk.length / incomingArrayBufferChunk.sampleRate,
+    );
+    const audioBuffersTableInfo = unplayedAudioBuffers.map((buffer, i) => {
+      return {
+        index: i,
+        duration: buffer.length / buffer.sampleRate,
+        samples: buffer.length,
+      };
+    });
+    const totalUnplayedDuration = unplayedAudioBuffers.reduce((acc, buffer) => {
+      return acc + buffer.length / buffer.sampleRate;
+    }, 0);
+
+    console.debug(
+      `[addAudioToBufferAndPlay] Current state of incoming audio buffers (${totalUnplayedDuration.toFixed(
+        1,
+      )}s unplayed):`,
+    );
+    console.table(audioBuffersTableInfo);
+
+    if (shouldPlayWhenAudioAvailable) {
+      playNextBufferIfNotAlreadyPlaying();
+    }
+  };
+
+  return {addAudioToBuffer, setGain, stop, start};
+}
diff --git a/streaming-test-app/src/cursorBlinkInterval.ts b/streaming-test-app/src/cursorBlinkInterval.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0d438028585312ca3c106cd34a071416aa8b1f62
--- /dev/null
+++ b/streaming-test-app/src/cursorBlinkInterval.ts
@@ -0,0 +1 @@
+export const CURSOR_BLINK_INTERVAL_MS = 500;
diff --git a/streaming-test-app/src/debug.ts b/streaming-test-app/src/debug.ts
new file mode 100644
index 0000000000000000000000000000000000000000..820ce46485f996fc998605bcb53045103286837a
--- /dev/null
+++ b/streaming-test-app/src/debug.ts
@@ -0,0 +1,257 @@
+import {TYPING_ANIMATION_DELAY_MS} from './StreamingInterface';
+import {getURLParams} from './URLParams';
+import audioBuffertoWav from 'audiobuffer-to-wav';
+import './StreamingInterface.css';
+
+type StartEndTime = {
+  start: number;
+  end: number;
+};
+
+type StartEndTimeWithAudio = StartEndTime & {
+  float32Audio: Float32Array;
+};
+
+type Text = {
+  time: number;
+  chars: number;
+};
+
+type DebugTimings = {
+  receivedAudio: StartEndTime[];
+  playedAudio: StartEndTimeWithAudio[];
+  receivedText: Text[];
+  renderedText: StartEndTime[];
+  sentAudio: StartEndTimeWithAudio[];
+  startRenderTextTime: number | null;
+  startRecordingTime: number | null;
+  receivedAudioSampleRate: number | null;
+};
+
+function getInitialTimings(): DebugTimings {
+  return {
+    receivedAudio: [],
+    playedAudio: [],
+    receivedText: [],
+    renderedText: [],
+    sentAudio: [],
+    startRenderTextTime: null,
+    startRecordingTime: null,
+    receivedAudioSampleRate: null,
+  };
+}
+
+function downloadAudioBuffer(audioBuffer: AudioBuffer, fileName: string): void {
+  const wav = audioBuffertoWav(audioBuffer);
+  const wavBlob = new Blob([new DataView(wav)], {
+    type: 'audio/wav',
+  });
+  const url = URL.createObjectURL(wavBlob);
+  const anchor = document.createElement('a');
+  anchor.href = url;
+  anchor.target = '_blank';
+  anchor.download = fileName;
+  anchor.click();
+}
+
+// Uncomment for debugging without download
+// function playAudioBuffer(audioBuffer: AudioBuffer): void {
+//   const audioContext = new AudioContext();
+//   const source = audioContext.createBufferSource();
+
+//   source.buffer = audioBuffer;
+//   source.connect(audioContext.destination);
+//   source.start();
+// }
+
+// Accumulate timings and audio / text translation samples for debugging and exporting
+class DebugTimingsManager {
+  timings: DebugTimings = getInitialTimings();
+
+  start(): void {
+    this.timings = getInitialTimings();
+    this.timings.startRecordingTime = new Date().getTime();
+  }
+
+  sentAudio(event: AudioProcessingEvent): void {
+    const end = new Date().getTime();
+    const start = end - event.inputBuffer.duration * 1000;
+    // Copy or else buffer seems to be re-used
+    const float32Audio = new Float32Array(event.inputBuffer.getChannelData(0));
+    this.timings.sentAudio.push({
+      start,
+      end,
+      float32Audio,
+    });
+  }
+
+  receivedText(text: string): void {
+    this.timings.receivedText.push({
+      time: new Date().getTime(),
+      chars: text.length,
+    });
+  }
+
+  startRenderText(): void {
+    if (this.timings.startRenderTextTime == null) {
+      this.timings.startRenderTextTime = new Date().getTime();
+    }
+  }
+
+  endRenderText(): void {
+    if (this.timings.startRenderTextTime == null) {
+      console.warn(
+        'Wrong timings of start / end rendering text. startRenderText is null',
+      );
+      return;
+    }
+
+    this.timings.renderedText.push({
+      start: this.timings.startRenderTextTime as number,
+      end: new Date().getTime(),
+    });
+    this.timings.startRenderTextTime = null;
+  }
+
+  receivedAudio(duration: number): void {
+    const start = new Date().getTime();
+    this.timings.receivedAudio.push({
+      start,
+      end: start + duration * 1000,
+    });
+  }
+
+  playedAudio(start: number, end: number, buffer: AudioBuffer | null): void {
+    if (buffer != null) {
+      if (this.timings.receivedAudioSampleRate == null) {
+        this.timings.receivedAudioSampleRate = buffer.sampleRate;
+      }
+      if (this.timings.receivedAudioSampleRate != buffer.sampleRate) {
+        console.error(
+          'Sample rates of received audio are unequal, will fail to reconstruct debug audio',
+          this.timings.receivedAudioSampleRate,
+          buffer.sampleRate,
+        );
+      }
+    }
+    this.timings.playedAudio.push({
+      start,
+      end,
+      float32Audio:
+        buffer == null
+          ? new Float32Array()
+          : new Float32Array(buffer.getChannelData(0)),
+    });
+  }
+
+  getChartData() {
+    const columns = [
+      {type: 'string', id: 'Series'},
+      {type: 'date', id: 'Start'},
+      {type: 'date', id: 'End'},
+    ];
+    return [
+      columns,
+      ...this.timings.sentAudio.map((sentAudio) => [
+        'Sent Audio',
+        new Date(sentAudio.start),
+        new Date(sentAudio.end),
+      ]),
+      ...this.timings.receivedAudio.map((receivedAudio) => [
+        'Received Audio',
+        new Date(receivedAudio.start),
+        new Date(receivedAudio.end),
+      ]),
+      ...this.timings.playedAudio.map((playedAudio) => [
+        'Played Audio',
+        new Date(playedAudio.start),
+        new Date(playedAudio.end),
+      ]),
+      // Best estimate duration by multiplying length with animation duration for each letter
+      ...this.timings.receivedText.map((receivedText) => [
+        'Received Text',
+        new Date(receivedText.time),
+        new Date(
+          receivedText.time + receivedText.chars * TYPING_ANIMATION_DELAY_MS,
+        ),
+      ]),
+      ...this.timings.renderedText.map((renderedText) => [
+        'Rendered Text',
+        new Date(renderedText.start),
+        new Date(renderedText.end),
+      ]),
+    ];
+  }
+
+  downloadInputAudio() {
+    const audioContext = new AudioContext();
+    const totalLength = this.timings.sentAudio.reduce((acc, cur) => {
+      return acc + cur?.float32Audio?.length ?? 0;
+    }, 0);
+    if (totalLength === 0) {
+      return;
+    }
+
+    const incomingArrayBuffer = audioContext.createBuffer(
+      1, // 1 channel
+      totalLength,
+      audioContext.sampleRate,
+    );
+
+    const buffer = incomingArrayBuffer.getChannelData(0);
+    let i = 0;
+    this.timings.sentAudio.forEach((sentAudio) => {
+      sentAudio.float32Audio.forEach((bytes) => {
+        buffer[i++] = bytes;
+      });
+    });
+
+    // Play for debugging
+    // playAudioBuffer(incomingArrayBuffer);
+    downloadAudioBuffer(incomingArrayBuffer, `input_audio.wav`);
+  }
+
+  downloadOutputAudio() {
+    const playedAudio = this.timings.playedAudio;
+    const sampleRate = this.timings.receivedAudioSampleRate;
+    if (
+      playedAudio.length === 0 ||
+      this.timings.startRecordingTime == null ||
+      sampleRate == null
+    ) {
+      return null;
+    }
+
+    let previousEndTime = this.timings.startRecordingTime;
+    const audioArray: number[] = [];
+    playedAudio.forEach((audio) => {
+      const delta = (audio.start - previousEndTime) / 1000;
+      for (let i = 0; i < delta * sampleRate; i++) {
+        audioArray.push(0.0);
+      }
+      audio.float32Audio.forEach((bytes) => audioArray.push(bytes));
+      previousEndTime = audio.end;
+    });
+    const audioContext = new AudioContext();
+    const incomingArrayBuffer = audioContext.createBuffer(
+      1, // 1 channel
+      audioArray.length,
+      sampleRate,
+    );
+
+    incomingArrayBuffer.copyToChannel(
+      new Float32Array(audioArray),
+      0, // first channel
+    );
+
+    // Play for debugging
+    // playAudioBuffer(incomingArrayBuffer);
+    downloadAudioBuffer(incomingArrayBuffer, 'output_audio.wav');
+  }
+}
+
+const debugSingleton = new DebugTimingsManager();
+export default function debug(): DebugTimingsManager | null {
+  const debugParam = getURLParams().debug;
+  return debugParam ? debugSingleton : null;
+}
diff --git a/streaming-test-app/src/float32To16BitPCM.ts b/streaming-test-app/src/float32To16BitPCM.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8205eefce0666d81abe5914d824da08b31b2314e
--- /dev/null
+++ b/streaming-test-app/src/float32To16BitPCM.ts
@@ -0,0 +1,16 @@
+export default function float32To16BitPCM(
+  float32Arr: Float32Array,
+): Int16Array {
+  const pcm16bit = new Int16Array(float32Arr.length);
+  for (let i = 0; i < float32Arr.length; ++i) {
+    // force number in [-1,1]
+    const s = Math.max(-1, Math.min(1, float32Arr[i]));
+
+    /**
+     * convert 32 bit float to 16 bit int pcm audio
+     * 0x8000 = minimum int16 value, 0x7fff = maximum int16 value
+     */
+    pcm16bit[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
+  }
+  return pcm16bit;
+}
diff --git a/streaming-test-app/src/generateNewRoomID.ts b/streaming-test-app/src/generateNewRoomID.ts
new file mode 100644
index 0000000000000000000000000000000000000000..49db3780c52261a5c990e82f73a696f24255a4ad
--- /dev/null
+++ b/streaming-test-app/src/generateNewRoomID.ts
@@ -0,0 +1,56 @@
+import {random} from 'lodash';
+
+// const USABLE_CHARACTERS = 'BCDFGHJKMPQRTVWXY2346789';
+const USABLE_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+const ID_LENGTH = 4;
+
+export function isValidRoomID(id: string | null | undefined): boolean {
+  if (id == null) {
+    return false;
+  }
+  if (id.length !== ID_LENGTH) {
+    return false;
+  }
+  return isValidPartialRoomID(id);
+}
+
+export function isValidPartialRoomID(roomID: string): boolean {
+  return (
+    roomID.length <= ID_LENGTH &&
+    roomID.split('').every((char) => USABLE_CHARACTERS.includes(char))
+  );
+}
+
+export default function generateNewRoomID(): string {
+  return Array.from(
+    {length: ID_LENGTH},
+    () => USABLE_CHARACTERS[random(USABLE_CHARACTERS.length - 1)],
+  ).join('');
+}
+
+export function getSequentialRoomIDForTestingGenerator(): () => string {
+  let counter = 0;
+
+  return function generateNextRoomID(): string {
+    const counterInBase: string = Number(counter)
+      .toString(USABLE_CHARACTERS.length)
+      .padStart(ID_LENGTH, '0');
+
+    if (counterInBase.length > ID_LENGTH) {
+      throw new Error(
+        'Ran out of unique room IDs from the sequential generator',
+      );
+    }
+
+    const result = counterInBase
+      .split('')
+      .map(
+        (digit) => USABLE_CHARACTERS[parseInt(digit, USABLE_CHARACTERS.length)],
+      )
+      .join('');
+
+    counter++;
+
+    return result;
+  };
+}
diff --git a/streaming-test-app/src/getParamFlag.ts b/streaming-test-app/src/getParamFlag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c469cc38c75fa10686f094548289c5034ee413f6
--- /dev/null
+++ b/streaming-test-app/src/getParamFlag.ts
@@ -0,0 +1,39 @@
+import type {URLParamNames} from './types/URLParamsTypes';
+
+export function getBooleanParamFlag(
+  flag: URLParamNames,
+  defaultValue?: boolean,
+): boolean {
+  const paramFlagValue = getBooleanParamFlagWithoutDefault(flag);
+
+  if (paramFlagValue == null) {
+    // The default value for paramFlags is false, unless they explicitly provide a
+    // defaultValue via the config
+    return defaultValue ?? false;
+  }
+
+  return paramFlagValue;
+}
+
+export function getBooleanParamFlagWithoutDefault(
+  flag: URLParamNames,
+): boolean | null {
+  const urlParams = new URLSearchParams(window.location.search);
+
+  if (urlParams.get(flag) == null) {
+    return null;
+  }
+
+  return urlParams.get(flag) !== '0';
+}
+
+export function getStringParamFlag(
+  flag: URLParamNames,
+  defaultValue?: string,
+): string | null {
+  const urlParams = new URLSearchParams(window.location.search);
+
+  const param = urlParams.get(flag);
+
+  return param ?? defaultValue ?? null;
+}
diff --git a/streaming-test-app/src/getTranslationSentencesFromReceivedData.ts b/streaming-test-app/src/getTranslationSentencesFromReceivedData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d7a6851f21e5c69dc2fd6428d21e9e86a0109000
--- /dev/null
+++ b/streaming-test-app/src/getTranslationSentencesFromReceivedData.ts
@@ -0,0 +1,22 @@
+import {ServerTextData, TranslationSentences} from './types/StreamingTypes';
+
+export default function getTranslationSentencesFromReceivedData(
+  receivedData: Array,
+): TranslationSentences {
+  return receivedData
+    .reduce(
+      (acc, data) => {
+        const newAcc = [
+          ...acc.slice(0, -1),
+          acc[acc.length - 1].trim() + ' ' + data.payload,
+        ];
+        if (data.eos) {
+          newAcc.push('');
+        }
+
+        return newAcc;
+      },
+      [''],
+    )
+    .filter((s) => s.trim().length !== 0);
+}
diff --git a/streaming-test-app/src/isScrolledToDocumentBottom.ts b/streaming-test-app/src/isScrolledToDocumentBottom.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90e3f2ff5a56c736e361e77e839f2897accec6d6
--- /dev/null
+++ b/streaming-test-app/src/isScrolledToDocumentBottom.ts
@@ -0,0 +1,11 @@
+export default function isScrolledToDocumentBottom(
+  bufferPx: number = 0,
+): boolean {
+  if (
+    window.innerHeight + window.scrollY >=
+    document.body.offsetHeight - bufferPx
+  ) {
+    return true;
+  }
+  return false;
+}
diff --git a/streaming-test-app/src/languageLookup.ts b/streaming-test-app/src/languageLookup.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe2abbc61cfad97e5a36896cad50446595d7e8a0
--- /dev/null
+++ b/streaming-test-app/src/languageLookup.ts
@@ -0,0 +1,119 @@
+const LANG3_TO_NAME = {
+  afr: 'afrikaans',
+  amh: 'amharic',
+  arb: 'arabic',
+  asm: 'assamese',
+  azj: 'azerbaijani',
+  bak: 'bashkir',
+  bel: 'belarusian',
+  ben: 'bengali',
+  bod: 'tibetan',
+  bos: 'bosnian',
+  bre: 'breton',
+  bul: 'bulgarian',
+  cat: 'catalan',
+  ces: 'czech',
+  cmn: 'chinese',
+  cym: 'welsh',
+  dan: 'danish',
+  deu: 'german',
+  ell: 'greek',
+  eng: 'english',
+  est: 'estonian',
+  eus: 'basque',
+  fao: 'faroese',
+  fin: 'finnish',
+  fra: 'french',
+  glg: 'galician',
+  guj: 'gujarati',
+  hat: 'haitian creole',
+  hau: 'hausa',
+  haw: 'hawaiian',
+  heb: 'hebrew',
+  hin: 'hindi',
+  hrv: 'croatian',
+  hun: 'hungarian',
+  hye: 'armenian',
+  ind: 'indonesian',
+  isl: 'icelandic',
+  ita: 'italian',
+  jav: 'javanese',
+  jpn: 'japanese',
+  kan: 'kannada',
+  kat: 'georgian',
+  kaz: 'kazakh',
+  khk: 'mongolian',
+  khm: 'khmer',
+  kor: 'korean',
+  lao: 'lao',
+  lat: 'latin',
+  lin: 'lingala',
+  lit: 'lithuanian',
+  ltz: 'luxembourgish',
+  lvs: 'latvian',
+  mal: 'malayalam',
+  mar: 'marathi',
+  mkd: 'macedonian',
+  mlg: 'malagasy',
+  mlt: 'maltese',
+  mri: 'maori',
+  mya: 'myanmar',
+  nld: 'dutch',
+  nno: 'nynorsk',
+  nob: 'norwegian',
+  npi: 'nepali',
+  oci: 'occitan',
+  pan: 'punjabi',
+  pbt: 'pashto',
+  pes: 'persian',
+  pol: 'polish',
+  por: 'portuguese',
+  ron: 'romanian',
+  rus: 'russian',
+  san: 'sanskrit',
+  sin: 'sinhala',
+  slk: 'slovak',
+  slv: 'slovenian',
+  sna: 'shona',
+  snd: 'sindhi',
+  som: 'somali',
+  spa: 'spanish',
+  sqi: 'albanian',
+  srp: 'serbian',
+  sun: 'sundanese',
+  swe: 'swedish',
+  swh: 'swahili',
+  tam: 'tamil',
+  tat: 'tatar',
+  tel: 'telugu',
+  tgk: 'tajik',
+  tgl: 'tagalog',
+  tha: 'thai',
+  tuk: 'turkmen',
+  tur: 'turkish',
+  ukr: 'ukrainian',
+  urd: 'urdu',
+  uzn: 'uzbek',
+  vie: 'vietnamese',
+  yid: 'yiddish',
+  yor: 'yoruba',
+  zlm: 'malay',
+};
+
+export function getLanguageFromThreeLetterCode(
+  lang3Code: string,
+): string | null {
+  try {
+    const name = LANG3_TO_NAME[lang3Code] ?? null;
+    if (name == null) {
+      return null;
+    }
+    const capitalizedWords = name
+      .split(' ')
+      .map((word: string) => word[0].toUpperCase() + word.slice(1));
+    return capitalizedWords.join(' ');
+  } catch (e) {
+    console.warn(`Unable to get language name for code ${lang3Code}: ${e}`);
+  }
+  return null;
+}
diff --git a/streaming-test-app/src/main.tsx b/streaming-test-app/src/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5775c051a6fd4ebad5f583ef8d1d6413a47aa46
--- /dev/null
+++ b/streaming-test-app/src/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App.tsx';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+  
+     ,
+);
diff --git a/streaming-test-app/src/react-xr/ARButton.tsx b/streaming-test-app/src/react-xr/ARButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9de536a3071aad466f2426f596012d32a35f2c8d
--- /dev/null
+++ b/streaming-test-app/src/react-xr/ARButton.tsx
@@ -0,0 +1,89 @@
+import * as THREE from 'three';
+import {Button} from '@mui/material';
+import {useCallback, useEffect, useState} from 'react';
+import {BufferedSpeechPlayer} from '../createBufferedSpeechPlayer';
+
+type Props = {
+  bufferedSpeechPlayer: BufferedSpeechPlayer;
+  renderer: THREE.WebGLRenderer | null;
+  onARVisible?: () => void;
+  onARHidden?: () => void;
+};
+
+export default function ARButton({
+  bufferedSpeechPlayer,
+  renderer,
+  onARVisible,
+  onARHidden,
+}: Props) {
+  const [session, setSession] = useState(null);
+  const [supported, setSupported] = useState(true);
+
+  useEffect(() => {
+    if (!navigator.xr) {
+      setSupported(false);
+      return;
+    }
+    navigator.xr.isSessionSupported('immersive-ar').then((supported) => {
+      setSupported(supported);
+    });
+  }, []);
+
+  const resetBuffers = useCallback(
+    (event: XRSessionEvent) => {
+      const session = event.target;
+      if (!(session instanceof XRSession)) {
+        return;
+      }
+      switch (session.visibilityState) {
+        case 'visible':
+          console.log('Restarting speech player, device is visible');
+          bufferedSpeechPlayer.stop();
+          bufferedSpeechPlayer.start();
+          onARVisible?.();
+          break;
+        case 'hidden':
+          console.log('Stopping speech player, device is hidden');
+          bufferedSpeechPlayer.stop();
+          bufferedSpeechPlayer.start();
+          onARHidden?.();
+          break;
+      }
+    },
+    [bufferedSpeechPlayer],
+  );
+
+  async function onSessionStarted(session: XRSession) {
+    setSession(session);
+
+    session.onvisibilitychange = resetBuffers;
+    session.onend = onSessionEnded;
+
+    await renderer.xr.setSession(session);
+  }
+
+  function onSessionEnded() {
+    setSession(null);
+  }
+
+  const onClick = () => {
+    if (session === null) {
+      navigator.xr!.requestSession('immersive-ar').then(onSessionStarted);
+    } else {
+      session.end();
+    }
+  };
+  return (
+    
+      {supported
+        ? renderer != null
+          ? 'Enter AR'
+          : 'Initializing AR...'
+        : 'AR Not Supported'}
+     
+  );
+}
diff --git a/streaming-test-app/src/react-xr/Button.tsx b/streaming-test-app/src/react-xr/Button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..98bc06f99d68470ea2d3de6df424fbb665b60755
--- /dev/null
+++ b/streaming-test-app/src/react-xr/Button.tsx
@@ -0,0 +1,117 @@
+import {useRef, useEffect} from 'react';
+import * as THREE from 'three';
+import {extend} from '@react-three/fiber';
+import ThreeMeshUI from 'three-mesh-ui';
+import ThreeMeshUIText, {ThreeMeshUITextType} from './ThreeMeshUIText';
+import {Interactive} from '@react-three/xr';
+
+/**
+ * Using `?url` at the end of this import tells vite this is a static asset, and
+ * provides us a URL to the hashed version of the file when the project is built.
+ * See: https://vitejs.dev/guide/assets.html#explicit-url-imports
+ */
+import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url';
+import robotoFontTexture from '../assets/RobotoMono-Regular.png';
+
+extend(ThreeMeshUI);
+
+/**
+ * Button component that renders as a three-mesh-ui block
+ */
+export default function Button({
+  onClick,
+  content,
+  width,
+  height,
+  fontSize,
+  borderRadius,
+  padding,
+}) {
+  const button = useRef();
+  const textRef = useRef();
+
+  useEffect(() => {
+    if (textRef.current != null) {
+      textRef.current.set({content});
+    }
+  }, [textRef, content]);
+
+  useEffect(() => {
+    if (!button.current) {
+      return;
+    }
+    button.current.setupState({
+      state: 'hovered',
+      attributes: {
+        offset: 0.002,
+        backgroundColor: new THREE.Color(0x607b8f),
+        fontColor: new THREE.Color(0xffffff),
+      },
+    });
+    button.current.setupState({
+      state: 'idle',
+      attributes: {
+        offset: 0.001,
+        backgroundColor: new THREE.Color(0x465a69),
+        fontColor: new THREE.Color(0xffffff),
+      },
+    });
+    button.current.setupState({
+      state: 'selected',
+      attributes: {
+        offset: 0.005,
+        backgroundColor: new THREE.Color(0x000000),
+        fontColor: new THREE.Color(0xffffff),
+      },
+    });
+    button.current.setState('idle');
+  }, []);
+
+  const args = [
+    {
+      width,
+      height,
+      fontSize,
+      padding,
+      justifyContent: 'end',
+      textAlign: 'center',
+      alignItems: 'center',
+      borderRadius,
+      fontFamily: robotoFontFamilyJson,
+      fontTexture: robotoFontTexture,
+      backgroundOpacity: 1,
+      backgroundColor: new THREE.Color(0x779092),
+      fontColor: new THREE.Color(0x000000),
+    },
+  ];
+
+  return (
+     {
+        onClick();
+      }}
+      onHover={() => button.current.setState('hovered')}
+      onBlur={() => button.current.setState('idle')}
+      onSelectStart={() => button.current.setState('selected')}
+      onSelectEnd={() => button.current.setState('idle')}>
+       button.current.setState('hovered')}
+        onPointerLeave={() => button.current.setState('idle')}
+        onPointerDown={() => button.current.setState('selected')}
+        onPointerUp={() => {
+          button.current.setState('hovered');
+          onClick();
+        }}>
+        
+           
+       
+     
+  );
+}
diff --git a/streaming-test-app/src/react-xr/Colors.ts b/streaming-test-app/src/react-xr/Colors.ts
new file mode 100644
index 0000000000000000000000000000000000000000..86cb3bbd427d4b9509463e0e1694d73b1295bf5d
--- /dev/null
+++ b/streaming-test-app/src/react-xr/Colors.ts
@@ -0,0 +1,6 @@
+import * as THREE from 'three';
+
+export const WHITE = new THREE.Color('#FFFFFF');
+export const BLACK = new THREE.Color('#000000');
+export const RED = new THREE.Color('red');
+export const BLUE = new THREE.Color('blue');
diff --git a/streaming-test-app/src/react-xr/MovementController.tsx b/streaming-test-app/src/react-xr/MovementController.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6a197112410b679bf6ea455c15f6d03897257afd
--- /dev/null
+++ b/streaming-test-app/src/react-xr/MovementController.tsx
@@ -0,0 +1,64 @@
+import {useRef} from 'react';
+import {useFrame} from '@react-three/fiber';
+import {useController, useXR} from '@react-three/xr';
+import * as THREE from 'three';
+
+const USE_HORIZONTAL = true;
+const USE_VERTICAL = true;
+const USE_ROTATION = true;
+const HORIZONTAL_AXIS = 2;
+const VERTICAL_AXIS = 3;
+const ROTATION_AXIS = 2;
+const SENSITIVITY = 0.05;
+const DEADZONE = 0.05;
+
+/**
+ * Component to add into the ThreeJS canvas that reads controller (Quest) inputs to change camera position
+ */
+export default function MovementController() {
+  const xr = useXR();
+  const controller = useController('right');
+  const forward = useRef(new THREE.Vector3());
+  const horizontal = useRef(new THREE.Vector3());
+
+  useFrame(() => {
+    const player = xr.player;
+    const camera = xr.player.children[0];
+    const cameraMatrix = camera.matrixWorld.elements;
+    forward.current
+      .set(-cameraMatrix[8], -cameraMatrix[9], -cameraMatrix[10])
+      .normalize();
+
+    const axes = controller?.inputSource?.gamepad?.axes ?? [0, 0, 0, 0];
+
+    if (USE_HORIZONTAL) {
+      horizontal.current.copy(forward.current);
+      horizontal.current.cross(camera.up).normalize();
+
+      player.position.add(
+        horizontal.current.multiplyScalar(
+          (Math.abs(axes[HORIZONTAL_AXIS]) > DEADZONE
+            ? axes[HORIZONTAL_AXIS]
+            : 0) * SENSITIVITY,
+        ),
+      );
+    }
+
+    if (USE_VERTICAL) {
+      player.position.add(
+        forward.current.multiplyScalar(
+          (Math.abs(axes[VERTICAL_AXIS]) > DEADZONE ? axes[VERTICAL_AXIS] : 0) *
+            SENSITIVITY,
+        ),
+      );
+    }
+
+    if (USE_ROTATION) {
+      player.rotation.y -=
+        (Math.abs(axes[ROTATION_AXIS]) > DEADZONE ? axes[ROTATION_AXIS] : 0) *
+        SENSITIVITY;
+    }
+  });
+
+  return <>>;
+}
diff --git a/streaming-test-app/src/react-xr/Playground.tsx b/streaming-test-app/src/react-xr/Playground.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5f03e1faca51e3225876d0b1805ff42454cbdaa3
--- /dev/null
+++ b/streaming-test-app/src/react-xr/Playground.tsx
@@ -0,0 +1,133 @@
+/**
+ * EXPERIMENTAL components to play around with but not officially use in the demo while
+ * we develop.
+ */
+import {useEffect, useState} from 'react';
+import {Object3DNode, extend} from '@react-three/fiber';
+import ThreeMeshUI from 'three-mesh-ui';
+
+import {} from '@react-three/xr';
+import {Sparkles, Shadow} from '@react-three/drei';
+
+// import FontImage from './assets/Roboto-msdf.png';
+import {FontLoader} from 'three/examples/jsm/loaders/FontLoader.js';
+import {TextGeometry} from 'three/examples/jsm/geometries/TextGeometry.js';
+import ThreeMeshUIText from './ThreeMeshUIText';
+import {ContactShadows, BakeShadows} from '@react-three/drei';
+
+extend({TextGeometry});
+extend(ThreeMeshUI);
+
+declare module '@react-three/fiber' {
+  interface ThreeElements {
+    textGeometry: Object3DNode;
+  }
+}
+
+// This is for textGeometry.. not using three-mesh-ui to display text
+export function TitleMesh() {
+  const font = new FontLoader().parse();
+  console.log('font', font);
+  const [text, setText] = useState('Text');
+
+  useEffect(() => {
+    setTimeout(() => {
+      setText(text + ' more ');
+      console.log('adding more tex..', text);
+    }, 1000);
+  }, [text]);
+
+  return (
+    
+       
+  );
+}
+
+export function Sphere({
+  size = 1,
+  amount = 50,
+  color = 'white',
+  emissive,
+  ...props
+}) {
+  return (
+    
+       
+  );
+}
+
+export function Title({accentColor}) {
+  return (
+    
+       
+  );
+}
+
+export function RandomComponents() {
+  return (
+    <>
+      (y);
+  // We are reusing text blocks so this keeps track of when we changed rows so we can restart animation
+  const lastIndex = useRef(index);
+  useEffect(() => {
+    if (index != lastIndex.current) {
+      lastIndex.current = index;
+      !isBottomLine && setScrollY(startY);
+    } else if (scrollY < y) {
+      setScrollY((prev) => prev + SCROLL_Y_DELTA);
+    }
+  }, [isBottomLine, index, scrollY, setScrollY, startY, y]);
+
+  const [cursorBlinkOn, setCursorBlinkOn] = useState(false);
+  useEffect(() => {
+    if (isBottomLine) {
+      const interval = setInterval(() => {
+        setCursorBlinkOn((prev) => !prev);
+      }, CURSOR_BLINK_INTERVAL_MS);
+
+      return () => clearInterval(interval);
+    } else {
+      setCursorBlinkOn(false);
+    }
+  }, [isBottomLine]);
+
+  const numChars = content.length;
+
+  if (cursorBlinkOn) {
+    content = content + '|';
+  }
+
+  // Accounting for potential cursor for block width (the +1)
+  const width =
+    (numChars + (isBottomLine ? 1.1 : 0) + (numChars < 10 ? 1 : 0)) *
+    CHAR_WIDTH;
+
+  const height = LINE_HEIGHT;
+
+  // This is needed to update text content (doesn't work if we just update the content prop)
+  const textRef = useRef();
+  useEffect(() => {
+    if (textRef.current != null) {
+      textRef.current.set({content});
+    }
+  }, [content, textRef, y, startY]);
+
+  // Width starts from 0 and goes 1/2 in each direction
+  const xPosition = width / 2 - MAX_WIDTH / 2 + OFFSET_WIDTH;
+  return (
+    <>
+      
+        
+           
+       
+    >
+  );
+}
+
+function initialTextBlockProps(count: number): TextBlockProps[] {
+  return Array.from({length: count}).map(() => {
+    // Push in non display blocks because mesh UI crashes if elements are add / removed from screen.
+    return {
+      y: Y_COORD_START,
+      startY: 0,
+      index: 0,
+      textOpacity: 0,
+      backgroundOpacity: 0,
+      width: MAX_WIDTH,
+      height: LINE_HEIGHT,
+      content: '',
+      isBottomLine: true,
+    };
+  });
+}
+
+export default function TextBlocks({
+  translationText,
+}: {
+  translationText: string;
+}) {
+  const transcriptStateRef = useRef({
+    textBlocksProps: initialTextBlockProps(NUM_LINES),
+    lastTranslationStringIndex: 0,
+    lastTranslationLineStartIndex: 0,
+    transcriptLines: [],
+    lastRenderTime: new Date().getTime(),
+  });
+
+  const transcriptState = transcriptStateRef.current;
+  const {textBlocksProps, lastTranslationStringIndex, lastRenderTime} =
+    transcriptState;
+
+  const [charsToRender, setCharsToRender] = useState(0);
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      const currentTime = new Date().getTime();
+      const charsToRender = Math.round(
+        ((currentTime - lastRenderTime) * CHARS_PER_SECOND) / 1000,
+      );
+      setCharsToRender(charsToRender);
+    }, RENDER_INTERVAL);
+
+    return () => clearInterval(interval);
+  }, [lastRenderTime]);
+
+  const currentTime = new Date().getTime();
+  if (charsToRender < 1) {
+    return textBlocksProps.map((props, idx) => (
+       since this has typescript issues because it collides with
+ * the native  SVG element. Simple enough so abstracting it away in this file
+ * so it could be used in other places with low risk. e.g:
+ * (
+  function ThreeMeshUIText(props, ref) {
+    return (skipARIntro);
+  return (
+    <>
+      
+        {getURLParams().ARTranscriptionType === 'single_block' ? (
+           
+    >
+  );
+}
+
+// Original UI that just uses a single block to render 6 lines in a panel
+function TranscriptPanelSingleBlock({
+  animateTextDisplay,
+  started,
+  translationSentences,
+  roomState,
+}: {
+  animateTextDisplay: boolean;
+  started: boolean;
+  translationSentences: TranslationSentences;
+  roomState: RoomState | null;
+}) {
+  const textRef = useRef();
+  const [didReceiveTranslationSentences, setDidReceiveTranslationSentences] =
+    useState(false);
+
+  const hasActiveTranscoders = (roomState?.activeTranscoders ?? 0) > 0;
+
+  const [cursorBlinkOn, setCursorBlinkOn] = useState(false);
+
+  // Normally we don't setState in render, but here we need to for computed state, and this if statement assures it won't loop infinitely
+  if (!didReceiveTranslationSentences && translationSentences.length > 0) {
+    setDidReceiveTranslationSentences(true);
+  }
+
+  const width = 1;
+  const height = 0.3;
+  const fontSize = 0.03;
+
+  useEffect(() => {
+    if (animateTextDisplay && hasActiveTranscoders) {
+      const interval = setInterval(() => {
+        setCursorBlinkOn((prev) => !prev);
+      }, CURSOR_BLINK_INTERVAL_MS);
+
+      return () => clearInterval(interval);
+    } else {
+      setCursorBlinkOn(false);
+    }
+  }, [animateTextDisplay, hasActiveTranscoders]);
+
+  useEffect(() => {
+    if (textRef.current != null) {
+      const initialPrompt =
+        'Welcome to the presentation. We are excited to share with you the work we have been doing... Our model can now translate languages in less than 2 second latency.';
+      // These are rough ratios based on spot checking
+      const maxLines = 6;
+      const charsPerLine = 55;
+
+      const transcriptSentences: string[] = didReceiveTranslationSentences
+        ? translationSentences
+        : [initialPrompt];
+
+      // The transcript is an array of sentences. For each sentence we break this down into an array of words per line.
+      // This is needed so we can "scroll" through without changing the order of words in the transcript
+      const linesToDisplay = transcriptSentences.flatMap((sentence, idx) => {
+        const blinkingCursor =
+          cursorBlinkOn && idx === transcriptSentences.length - 1 ? '|' : ' ';
+        const words = sentence.concat(blinkingCursor).split(/\s+/);
+        // Here we break each sentence up with newlines so all words per line fit within the panel
+        return words.reduce(
+          (wordChunks, currentWord) => {
+            const filteredWord = [...currentWord]
+              .filter((c) => {
+                if (supportedCharSet().has(c)) {
+                  return true;
+                }
+                console.error(
+                  `Unsupported char ${c} - make sure this is supported in the font family msdf file`,
+                );
+                return false;
+              })
+              .join('');
+            const lastLineSoFar = wordChunks[wordChunks.length - 1];
+            const charCount = lastLineSoFar.length + filteredWord.length + 1;
+            if (charCount <= charsPerLine) {
+              wordChunks[wordChunks.length - 1] =
+                lastLineSoFar + ' ' + filteredWord;
+            } else {
+              wordChunks.push(filteredWord);
+            }
+            return wordChunks;
+          },
+          [''],
+        );
+      });
+
+      // Only keep the last maxLines so new text keeps scrolling up from the bottom
+      linesToDisplay.splice(0, linesToDisplay.length - maxLines);
+      textRef.current.set({content: linesToDisplay.join('\n')});
+    }
+  }, [
+    translationSentences,
+    textRef,
+    didReceiveTranslationSentences,
+    cursorBlinkOn,
+  ]);
+
+  const opacity = started ? 1 : 0;
+  return (
+    
+      
+         
+     
+  );
+}
+
+// Splits up the lines into separate blocks to treat each one separately.
+// This allows changing of opacity, animating per line, changing height / width per line etc
+function TranscriptPanelBlocks({
+  translationSentences,
+}: {
+  translationSentences: TranslationSentences;
+}) {
+  return (
+    
+         
+      
+         
+      
+         setStarted(true)}
+          content={'Start Experience'}
+          width={0.2}
+          height={0.035}
+          fontSize={0.015}
+          padding={0.01}
+          borderRadius={0.01}
+        />
+        
+    >
+  );
+}
+
+export type XRConfigProps = {
+  animateTextDisplay: boolean;
+  bufferedSpeechPlayer: BufferedSpeechPlayer;
+  translationSentences: TranslationSentences;
+  roomState: RoomState | null;
+  roomID: string | null;
+  startStreaming: () => Promise;
+  stopStreaming: () => Promise;
+  debugParam: boolean | null;
+  onARVisible?: () => void;
+  onARHidden?: () => void;
+};
+
+export default function XRConfig(props: XRConfigProps) {
+  const {bufferedSpeechPlayer, debugParam} = props;
+  const skipARIntro = getURLParams().skipARIntro;
+  const defaultDimensions = {width: 500, height: 500};
+  const [dimensions, setDimensions] = useState(
+    debugParam ? defaultDimensions : {width: 0, height: 0},
+  );
+  const {width, height} = dimensions;
+
+  // Make sure to reset buffer when headset is taken off / on so we don't get an endless stream
+  // of audio. The oculus actually runs for some time after the headset is taken off.
+  const resetBuffers = useCallback(
+    (event: XREvent) => {
+      const session = event.target;
+      if (!(session instanceof XRSession)) {
+        return;
+      }
+      switch (session.visibilityState) {
+        case 'visible':
+          bufferedSpeechPlayer.start();
+          break;
+        case 'hidden':
+          bufferedSpeechPlayer.stop();
+          break;
+      }
+    },
+    [bufferedSpeechPlayer],
+  );
+
+  return (
+    
+      {/* This is the button that triggers AR flow if available via a button */}
+      
 console.error(e)}
+        onClick={() => setDimensions(defaultDimensions)}
+        style={{
+          position: 'absolute',
+          bottom: '24px',
+          left: '50%',
+          transform: 'translateX(-50%)',
+          padding: '12px 24px',
+          border: '1px solid white',
+          borderRadius: '4px',
+          backgroundColor: '#465a69',
+          color: 'white',
+          font: 'normal 0.8125rem sans-serif',
+          outline: 'none',
+          zIndex: 99999,
+          cursor: 'pointer',
+        }}
+      />
+      {/* Canvas to draw if in browser but if in AR mode displays in pass through mode */}
+      {/* The camera here just works in 2D mode. In AR mode it starts at at origin */}
+      {/*  */}
+      
+        
+          {/*
+            Uncomment this for controllers to show up
+             
+       
+      (null);
+  const canvasRef = useRef(null);
+  useEffect(() => {
+    if (canvasRef.current != null || debugParam === false) {
+      const existingRenderer = getRenderer();
+      if (existingRenderer) {
+        setRenderer(existingRenderer);
+      } else {
+        const newRenderer = init(
+          400,
+          300,
+          debugParam ? canvasRef.current : null,
+        );
+        setRenderer(newRenderer);
+      }
+    }
+  }, [canvasRef.current]);
+
+  return (
+    
+      
+        Welcome to the Seamless team streaming demo experience! In this demo you
+        will experience AI powered text and audio translation in real time.
+       
+      
+       
+  );
+}
+
+export default function XRDialog(props: XRConfigProps) {
+  const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+  return (
+    <>
+       setIsDialogOpen(true)}>
+        Enter AR Experience
+       
+      {isDialogOpen && (
+         setIsDialogOpen(false)} open={true}>
+          
+            FAIR Seamless Streaming Demo
+           
+           setIsDialogOpen(false)}
+            sx={{
+              position: 'absolute',
+              right: 8,
+              top: 8,
+              color: (theme) => theme.palette.grey[500],
+            }}>
+             
+           
+      )}
+    >
+  );
+}
diff --git a/streaming-test-app/src/react-xr/XRRendering.ts b/streaming-test-app/src/react-xr/XRRendering.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7af665a44cc97cd7dbd75cfc5a011565f1f4f251
--- /dev/null
+++ b/streaming-test-app/src/react-xr/XRRendering.ts
@@ -0,0 +1,402 @@
+import * as THREE from 'three';
+import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
+
+import ThreeMeshUI, {Block, Text} from 'three-mesh-ui';
+
+import FontJSON from '../assets/RobotoMono-Regular-msdf.json?url';
+import FontImage from '../assets/RobotoMono-Regular.png';
+import {TranslationSentences} from '../types/StreamingTypes';
+import supportedCharSet from './supportedCharSet';
+
+// Augment three-mesh-ui types which aren't implemented
+declare module 'three-mesh-ui' {
+  interface Block {
+    add(any: any);
+    set(props: BlockOptions);
+    position: {
+      x: number;
+      y: number;
+      z: number;
+      set: (x: number, y: number, z: number) => void;
+    };
+  }
+  interface Text {
+    set(props: {content: string});
+  }
+}
+
+// Various configuration parameters
+const INITIAL_PROMPT = 'Listening...\n';
+const NUM_LINES = 3;
+const CHARS_PER_LINE = 37;
+const CHARS_PER_SECOND = 15;
+
+const MAX_WIDTH = 0.89;
+const CHAR_WIDTH = 0.0233;
+const Y_COORD_START = -0.38;
+const Z_COORD = -1.3;
+const LINE_HEIGHT = 0.062;
+const BLOCK_SPACING = 0.02;
+const FONT_SIZE = 0.038;
+
+// Speed of scrolling of text lines
+const SCROLL_Y_DELTA = 0.01;
+
+// Overlay an extra block for padding due to inflexibilities of native padding
+const OFFSET = 0.01;
+const OFFSET_WIDTH = OFFSET * 3;
+
+// The tick interval
+const CURSOR_BLINK_INTERVAL_MS = 500;
+
+type TranscriptState = {
+  translationText: string;
+  textBlocksProps: TextBlockProps[];
+  lastTranslationStringIndex: number;
+  lastTranslationLineStartIndex: number;
+  transcriptLines: string[];
+  lastUpdateTime: number;
+};
+
+type TextBlockProps = {
+  content: string;
+  // The end position when animating
+  targetY: number;
+  // Current scroll position that caps at targetY
+  currentY: number;
+  textOpacity: number;
+  backgroundOpacity: number;
+  index: number;
+  isBottomLine: boolean;
+};
+
+function initialTextBlockProps(count: number): TextBlockProps[] {
+  return Array.from({length: count}).map(() => {
+    // Push in non display blocks because mesh UI crashes if elements are add / removed from screen.
+
+    return {
+      // key: textBlocksProps.length,
+      targetY: Y_COORD_START,
+      currentY: Y_COORD_START,
+      index: 0,
+      textOpacity: 0,
+      backgroundOpacity: 0,
+      width: MAX_WIDTH,
+      height: LINE_HEIGHT,
+      content: '',
+      isBottomLine: true,
+    };
+  });
+}
+
+function initialState(): TranscriptState {
+  return {
+    translationText: '',
+    textBlocksProps: initialTextBlockProps(NUM_LINES),
+    lastTranslationStringIndex: 0,
+    lastTranslationLineStartIndex: 0,
+    transcriptLines: [],
+    lastUpdateTime: new Date().getTime(),
+  };
+}
+
+let transcriptState: TranscriptState = initialState();
+
+let scene: THREE.Scene | null;
+let camera: THREE.PerspectiveCamera | null;
+let renderer: THREE.WebGLRenderer | null;
+let controls: THREE.OrbitControls | null;
+
+let cursorBlinkOn: boolean = false;
+
+setInterval(() => {
+  cursorBlinkOn = !cursorBlinkOn;
+}, CURSOR_BLINK_INTERVAL_MS);
+
+type TextBlock = {
+  textBlockOuterContainer: Block;
+  textBlockInnerContainer: Block;
+  text: Text;
+};
+const textBlocks: TextBlock[] = [];
+
+export function getRenderer(): THREE.WebGLRenderer | null {
+  return renderer;
+}
+
+export function init(
+  width: number,
+  height: number,
+  parentElement: HTMLDivElement | null,
+): THREE.WebGLRenderer {
+  scene = new THREE.Scene();
+  scene.background = new THREE.Color(0x505050);
+
+  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
+  camera.position.z = 1;
+
+  renderer = new THREE.WebGLRenderer({
+    antialias: true,
+  });
+  renderer.setPixelRatio(window.devicePixelRatio);
+  renderer.setSize(width, height);
+  renderer.xr.enabled = true;
+
+  renderer.xr.setReferenceSpaceType('local');
+
+  parentElement?.appendChild(renderer.domElement);
+
+  controls = new OrbitControls(camera, renderer.domElement);
+  controls.update();
+
+  scene.add(camera);
+
+  textBlocks.push(
+    ...initialTextBlockProps(NUM_LINES).map((props) => makeTextBlock(props)),
+  );
+
+  renderer.setAnimationLoop(loop);
+  return renderer;
+}
+
+export function updatetranslationText(
+  translationSentences: TranslationSentences,
+): void {
+  const newText = INITIAL_PROMPT + translationSentences.join('\n');
+  if (transcriptState.translationText === newText) {
+    return;
+  }
+  transcriptState.translationText = newText;
+}
+
+export function resetState(): void {
+  transcriptState = initialState();
+}
+
+function makeTextBlock({
+  content,
+  backgroundOpacity,
+}: TextBlockProps): TextBlock {
+  const width = MAX_WIDTH;
+  const height = LINE_HEIGHT;
+
+  const fontProps = {
+    fontSize: FONT_SIZE,
+    textAlign: 'left',
+    // TODO: support more language charsets
+    // This renders using MSDF format supported in WebGL. Renderable characters are defined in the "charset" json
+    // Currently supports most default keyboard inputs but this would exclude many non latin charset based languages.
+    // You can use https://msdf-bmfont.donmccurdy.com/ for easily generating these files
+    fontFamily: FontJSON,
+    fontTexture: FontImage,
+  };
+
+  const textBlockOuterContainer = new Block({
+    backgroundOpacity,
+    width: width + OFFSET_WIDTH,
+    height: height,
+    borderRadius: 0,
+    ...fontProps,
+  });
+
+  const text = new Text({content});
+  const textBlockInnerContainer = new Block({
+    padding: 0,
+    backgroundOpacity: 0,
+    width,
+    height,
+  });
+
+  // Adding it to the camera makes the UI follow it.
+  camera.add(textBlockOuterContainer);
+  textBlockOuterContainer.add(textBlockInnerContainer);
+  textBlockInnerContainer.add(text);
+
+  return {
+    textBlockOuterContainer,
+    textBlockInnerContainer,
+    text,
+  };
+}
+
+// Updates the position and text of a text block from its props
+function updateTextBlock(
+  id: number,
+  {content, targetY, currentY, backgroundOpacity, isBottomLine}: TextBlockProps,
+): void {
+  const {textBlockOuterContainer, textBlockInnerContainer, text} =
+    textBlocks[id];
+
+  const {lastTranslationStringIndex, translationText} = transcriptState;
+
+  // Add blinking cursor if we don't have any new input to render
+  const numChars = content.length;
+
+  if (
+    isBottomLine &&
+    cursorBlinkOn &&
+    lastTranslationStringIndex >= translationText.length
+  ) {
+    content = content + '|';
+  }
+
+  // Accounting for potential cursor for block width (the +1)
+  const width =
+    (numChars + (isBottomLine ? 1.1 : 0) + (numChars < 10 ? 1 : 0)) *
+    CHAR_WIDTH;
+  const height = LINE_HEIGHT;
+
+  // Width starts from 0 and goes 1/2 in each direction so offset x
+  const xPosition = width / 2 - MAX_WIDTH / 2 + OFFSET_WIDTH;
+  textBlockOuterContainer?.set({
+    backgroundOpacity,
+    width: width + 2 * OFFSET_WIDTH,
+    height: height + OFFSET / 3,
+    borderRadius: 0,
+  });
+
+  // Scroll up line toward target
+  const y = isBottomLine
+    ? targetY
+    : Math.min(currentY + SCROLL_Y_DELTA, targetY);
+  transcriptState.textBlocksProps[id].currentY = y;
+
+  textBlockOuterContainer.position.set(-OFFSET_WIDTH + xPosition, y, Z_COORD);
+  textBlockInnerContainer.set({
+    padding: 0,
+    backgroundOpacity: 0,
+    width,
+    height,
+  });
+  text.set({content});
+}
+
+// We split the text so it fits line by line into the UI
+function chunkTranslationTextIntoLines(
+  translationText: string,
+  nextTranslationStringIndex: number,
+): string[] {
+  // Ideally we continue where we left off but this is complicated when we have mid-words. Recalculating for now
+  const newSentences = translationText
+    .substring(0, nextTranslationStringIndex)
+    .split('\n');
+  const transcriptLines = [''];
+  newSentences.forEach((newSentence, sentenceIdx) => {
+    const words = newSentence.split(/\s+/);
+    words.forEach((word) => {
+      const filteredWord = [...word]
+        .filter((c) => {
+          if (supportedCharSet().has(c)) {
+            return true;
+          }
+          console.error(
+            `Unsupported char ${c} - make sure this is supported in the font family msdf file`,
+          );
+          return false;
+        })
+        .join('')
+        // Filter out unknown symbol
+        .replace('', '');
+
+      const lastLineSoFar = transcriptLines[0];
+      const charCount = lastLineSoFar.length + filteredWord.length + 1;
+
+      if (charCount <= CHARS_PER_LINE) {
+        transcriptLines[0] = lastLineSoFar + ' ' + filteredWord;
+      } else {
+        transcriptLines.unshift(filteredWord);
+      }
+    });
+
+    if (sentenceIdx < newSentences.length - 1) {
+      transcriptLines.unshift('\n');
+      transcriptLines.unshift('');
+    }
+  });
+  return transcriptLines;
+}
+
+// The main loop,
+function updateTextBlocksProps(): void {
+  const {translationText, lastTranslationStringIndex, lastUpdateTime} =
+    transcriptState;
+
+  const currentTime = new Date().getTime();
+  const charsToRender = Math.round(
+    ((currentTime - lastUpdateTime) * CHARS_PER_SECOND) / 1000,
+  );
+
+  if (charsToRender < 1) {
+    // Wait some more until we render more characters
+    return;
+  }
+
+  const nextTranslationStringIndex = Math.min(
+    lastTranslationStringIndex + charsToRender,
+    translationText.length,
+  );
+  if (nextTranslationStringIndex === lastTranslationStringIndex) {
+    // No new characters to render
+    transcriptState.lastUpdateTime = currentTime;
+    return;
+  }
+
+  // Ideally we continue where we left off but this is complicated when we have mid-words. Recalculating for now
+  const transcriptLines = chunkTranslationTextIntoLines(
+    translationText,
+    nextTranslationStringIndex,
+  );
+  transcriptState.transcriptLines = transcriptLines;
+  transcriptState.lastTranslationStringIndex = nextTranslationStringIndex;
+
+  // Compute the new props for each text block
+  const newTextBlocksProps: TextBlockProps[] = [];
+  // We start with the most recent line and increment the y coordinate for older lines.
+  // If it is a new sentence we increment the y coordinate a little more to leave a visible space
+  let y = Y_COORD_START;
+  transcriptLines.forEach((line, i) => {
+    if (newTextBlocksProps.length == NUM_LINES) {
+      return;
+    }
+
+    if (line === '\n') {
+      y += BLOCK_SPACING;
+      return;
+    }
+
+    const isBottomLine = newTextBlocksProps.length === 0;
+
+    const textOpacity = 1 - 0.1 * newTextBlocksProps.length;
+
+    const previousProps = transcriptState.textBlocksProps.find(
+      (props) => props.index === i,
+    );
+    const props = {
+      targetY: y + LINE_HEIGHT / 2,
+      currentY: isBottomLine ? y : previousProps?.currentY || y,
+      index: i,
+      textOpacity,
+      backgroundOpacity: 1,
+      content: line,
+      isBottomLine,
+    };
+    newTextBlocksProps.push(props);
+
+    y += LINE_HEIGHT;
+  });
+
+  transcriptState.textBlocksProps = newTextBlocksProps;
+  transcriptState.lastUpdateTime = currentTime;
+}
+
+// The main render loop, everything gets rendered here.
+function loop() {
+  updateTextBlocksProps();
+
+  transcriptState.textBlocksProps.map((props, i) => updateTextBlock(i, props));
+
+  ThreeMeshUI.update();
+
+  controls.update();
+  renderer.render(scene, camera);
+}
diff --git a/streaming-test-app/src/react-xr/supportedCharSet.ts b/streaming-test-app/src/react-xr/supportedCharSet.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8131ac5c44a1cc44a7b42d439da307afe63cef5b
--- /dev/null
+++ b/streaming-test-app/src/react-xr/supportedCharSet.ts
@@ -0,0 +1,20 @@
+import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url';
+
+async function fetchSupportedCharSet(): Promise> {
+  try {
+    const response = await fetch(robotoFontFamilyJson);
+    const fontFamily = await response.json();
+
+    return new Set(fontFamily.info.charset);
+  } catch (e) {
+    console.error('Failed to fetch supported XR charset', e);
+    return new Set();
+  }
+}
+
+let charSet = new Set();
+fetchSupportedCharSet().then((result) => (charSet = result));
+
+export default function supportedCharSet(): Set {
+  return charSet;
+}
diff --git a/streaming-test-app/src/setURLParam.ts b/streaming-test-app/src/setURLParam.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e58c8af1e52e5d2a5d3e21948e5d294eaf4c27c5
--- /dev/null
+++ b/streaming-test-app/src/setURLParam.ts
@@ -0,0 +1,40 @@
+export default function setURLParam(
+  paramName: string,
+  value: T,
+  // If there's no defaultValue specified then we always set the URL param explicitly
+  defaultValue?: T,
+): void {
+  const urlParams = new URLSearchParams(window.location.search);
+  if (defaultValue != null && value === defaultValue) {
+    urlParams.delete(paramName);
+  } else {
+    let stringValue: string;
+
+    switch (typeof value) {
+      case 'string':
+        stringValue = value;
+        break;
+      case 'boolean':
+        stringValue = value ? '1' : '0';
+        break;
+      default:
+        throw new Error(`Unsupported URL param type: ${typeof value}`);
+    }
+
+    if (urlParams.has(paramName)) {
+      urlParams.set(paramName, stringValue);
+    } else {
+      urlParams.append(paramName, stringValue);
+    }
+  }
+
+  const paramStringWithoutQuestionMark = urlParams.toString();
+
+  window.history.replaceState(
+    null,
+    '',
+    `${window.location.pathname}${
+      paramStringWithoutQuestionMark.length > 0 ? '?' : ''
+    }${paramStringWithoutQuestionMark}`,
+  );
+}
diff --git a/streaming-test-app/src/sliceTranslationSentencesUtils.ts b/streaming-test-app/src/sliceTranslationSentencesUtils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..daee955ce91d3ac73061b61364ab0abfbd6b2f79
--- /dev/null
+++ b/streaming-test-app/src/sliceTranslationSentencesUtils.ts
@@ -0,0 +1,30 @@
+import {TranslationSentences} from './types/StreamingTypes';
+
+export function getTotalSentencesLength(
+  translatedSentences: TranslationSentences,
+) {
+  return translatedSentences.reduce((acc, curr) => acc + curr.length, 0);
+}
+
+/**
+ * @returns A new array of strings where the total length of the strings === targetIndex,
+ * aka it's as if we joined all the strings together, called joined.slice(0, targetIndex), and then
+ * split the string back into an array of strings.
+ */
+export function sliceTranslationSentencesUpToIndex(
+  translatedSentences: TranslationSentences,
+  targetIndex: number,
+): TranslationSentences {
+  return translatedSentences.reduce((acc, sentence) => {
+    const accTotalLength = getTotalSentencesLength(acc);
+    if (accTotalLength === targetIndex) {
+      return acc;
+    }
+    // If adding the current sentence does not exceed the targetIndex, then add the whole sentence
+    if (accTotalLength + sentence.length <= targetIndex) {
+      return [...acc, sentence];
+    }
+    // If adding the current sentence DOES exceed the targetIndex, then slice the sentence and add it
+    return [...acc, sentence.slice(0, targetIndex - accTotalLength)];
+  }, []);
+}
diff --git a/streaming-test-app/src/theme.ts b/streaming-test-app/src/theme.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a4299e9b17dc3052bbf829b9e0f8267d735ad181
--- /dev/null
+++ b/streaming-test-app/src/theme.ts
@@ -0,0 +1,40 @@
+import {createTheme} from '@mui/material/styles';
+
+const themeObject = {
+  palette: {
+    background: {default: '#383838'},
+    primary: {
+      main: '#465A69',
+    },
+    info: {
+      main: '#0064E0',
+    },
+    text: {primary: '#1C2A33'},
+  },
+  typography: {
+    fontFamily: [
+      'Optimistic Text',
+      'Roboto',
+      '"Helvetica Neue"',
+      'Arial',
+      'sans-serif',
+    ].join(','),
+    h1: {fontSize: '1rem', fontWeight: '500'},
+  },
+};
+
+const theme = createTheme(themeObject);
+
+/**
+ * Set up a responsive font size at the 600px breakpoint
+ */
+// default is 1rem (16px)
+theme.typography.body1[theme.breakpoints.down('sm')] = {fontSize: '0.875rem'};
+// default is 1rem (16px)
+theme.typography.h1[theme.breakpoints.down('sm')] = {fontSize: '0.875rem'};
+// default is 0.875rem (14px)
+theme.typography.button[theme.breakpoints.down('sm')] = {fontSize: '0.75rem'};
+// default is 0.875rem (14px)
+theme.typography.body2[theme.breakpoints.down('sm')] = {fontSize: '0.75rem'};
+
+export default theme;
diff --git a/streaming-test-app/src/types/RoomState.ts b/streaming-test-app/src/types/RoomState.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae074547413481f58c5004108acd43c0b0c70d8d
--- /dev/null
+++ b/streaming-test-app/src/types/RoomState.ts
@@ -0,0 +1,16 @@
+export type MemberID = string;
+
+export type Member = {
+  client_id: MemberID;
+  session_id: string;
+  name: string;
+  connection_status: 'connected' | 'disconnected';
+};
+
+export type RoomState = {
+  activeTranscoders: number;
+  room_id: string;
+  members: Array;
+  listeners: Array;
+  speakers: Array;
+};
diff --git a/streaming-test-app/src/types/StreamingTypes.ts b/streaming-test-app/src/types/StreamingTypes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..800af114d13e2ce1a861c71ebb05e78597921799
--- /dev/null
+++ b/streaming-test-app/src/types/StreamingTypes.ts
@@ -0,0 +1,131 @@
+interface ServerTranslationDataBase {
+  eos: boolean;
+  event: string;
+  latency?: number;
+}
+
+export interface ServerTextData extends ServerTranslationDataBase {
+  event: 'translation_text';
+  payload: string;
+}
+
+export interface ServerSpeechData extends ServerTranslationDataBase {
+  event: 'translation_speech';
+  payload: Array;
+  sample_rate: number;
+}
+
+export const OUTPUT_MODALITIES_BASE_VALUES = ['s2t', 's2s'] as const;
+export type OutputModalitiesBase =
+  (typeof OUTPUT_MODALITIES_BASE_VALUES)[number];
+
+export const DYNAMIC_PARAMS_VALUES = ['expressive'] as const;
+export type DynamicParams = (typeof DYNAMIC_PARAMS_VALUES)[number];
+
+export type AgentCapabilities = {
+  name: string;
+  description: string;
+  modalities: Array;
+  targetLangs: Array;
+  dynamicParams: Array;
+};
+
+export const SUPPORTED_OUTPUT_MODE_VALUES = ['s2s&t', 's2t', 's2s'] as const;
+
+export type SupportedOutputMode = (typeof SUPPORTED_OUTPUT_MODE_VALUES)[number];
+
+export const SUPPORTED_OUTPUT_MODES: Array<{
+  value: (typeof SUPPORTED_OUTPUT_MODE_VALUES)[number];
+  label: string;
+}> = [
+    { value: 's2s&t', label: 'Text & Speech' },
+    { value: 's2t', label: 'Text' },
+    { value: 's2s', label: 'Speech' },
+  ];
+
+export const SUPPORTED_INPUT_SOURCE_VALUES = [
+  'userMedia',
+  'displayMedia',
+] as const;
+
+export type SupportedInputSource =
+  (typeof SUPPORTED_INPUT_SOURCE_VALUES)[number];
+
+export const SUPPORTED_INPUT_SOURCES: Array<{
+  value: SupportedInputSource;
+  label: string;
+}> = [
+  {value: 'userMedia', label: 'Microphone'},
+  {value: 'displayMedia', label: 'Browser Tab (Chrome only)'},
+];
+
+export type StartStreamEventConfig = {
+  event: 'config';
+  rate: number;
+  model_name: string;
+  debug: boolean;
+  async_processing: boolean;
+  model_type: SupportedOutputMode;
+  buffer_limit: number;
+};
+
+export interface BrowserAudioStreamConfig {
+  echoCancellation: boolean;
+  noiseSuppression: boolean;
+  echoCancellation: boolean;
+}
+
+export interface ServerStateItem {
+  activeConnections: number;
+  activeTranscoders: number;
+}
+
+export type ServerLockObject = {
+  name: string | null;
+  clientID: string | null;
+  isActive: boolean;
+};
+
+export type ServerState = ServerStateItem & {
+  agentsCapabilities: Array;
+  statusByRoom: {
+    [key: string]: { activeConnections: number; activeTranscoders: number };
+  };
+  totalActiveConnections: number;
+  totalActiveTranscoders: number;
+  serverLock: ServerLockObject | null;
+};
+
+export type ServerExceptionData = {
+  message: string;
+  timeEpochMs: number;
+  // NOTE: This is added on the client
+  timeStringClient?: string;
+  room?: string;
+  member?: string;
+  clientID?: string;
+};
+
+export type StreamingStatus = 'stopped' | 'running' | 'starting';
+
+export type TranslationSentences = Array;
+
+export type DynamicConfig = {
+  // targetLanguage: a 3-letter string representing the desired output language.
+  targetLanguage: string;
+  expressive: boolean | null;
+};
+
+export type PartialDynamicConfig = Partial;
+
+export type BaseResponse = {
+  status: 'ok' | 'error';
+  message: string;
+};
+
+export type Roles = 'speaker' | 'listener';
+
+export type JoinRoomConfig = {
+  roles: Array;
+  lockServerName: string | null;
+};
diff --git a/streaming-test-app/src/types/URLParamsTypes.ts b/streaming-test-app/src/types/URLParamsTypes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da5d23f81166b2881bff81ef75e1fc2bb7d3eced
--- /dev/null
+++ b/streaming-test-app/src/types/URLParamsTypes.ts
@@ -0,0 +1,16 @@
+export type URLParamsObject = {
+  animateTextDisplay: boolean;
+  autoJoin: boolean;
+  debug: boolean;
+  enableServerLock: boolean;
+  roomID: string | null;
+  serverURL: string | null;
+  skipARIntro: boolean;
+  ARTranscriptionType:
+  | 'single_block'
+  | 'lines'
+  | 'lines_with_background'
+  | string;
+};
+
+export type URLParamNames = keyof URLParamsObject;
diff --git a/streaming-test-app/src/types/exhaustivenessCheck.ts b/streaming-test-app/src/types/exhaustivenessCheck.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a794e637329a1c5e44a3380abd157b84cfdfd9aa
--- /dev/null
+++ b/streaming-test-app/src/types/exhaustivenessCheck.ts
@@ -0,0 +1,6 @@
+// Useful for ensuring switch statements are exhaustive
+export default function exhaustivenessCheck(p: never): never {
+  throw new Error(
+    `This should never happen. Value received: ${JSON.stringify(p)}`,
+  );
+}
diff --git a/streaming-test-app/src/useSocket.ts b/streaming-test-app/src/useSocket.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8a9794047120c17f4bcc1e1962c51cc94512d5c
--- /dev/null
+++ b/streaming-test-app/src/useSocket.ts
@@ -0,0 +1,18 @@
+import {createContext, useContext} from 'react';
+import {Socket} from 'socket.io-client';
+
+type SocketObject = {
+  socket: Socket | null;
+  clientID: string | null;
+  connected: boolean;
+};
+
+export const SocketContext = createContext({
+  socket: null,
+  clientID: null,
+  connected: false,
+});
+
+export function useSocket(): SocketObject {
+  return useContext(SocketContext);
+}
diff --git a/streaming-test-app/src/useStable.ts b/streaming-test-app/src/useStable.ts
new file mode 100644
index 0000000000000000000000000000000000000000..789f9168bed2687f244f5dc3ea1ed3bce6517c3e
--- /dev/null
+++ b/streaming-test-app/src/useStable.ts
@@ -0,0 +1,15 @@
+import {useRef} from 'react';
+
+type UninitializedMarker = Readonly> | symbol;
+const UNINITIALIZED: UninitializedMarker =
+  typeof Symbol === 'function' && typeof Symbol() === 'symbol'
+    ? Symbol()
+    : Object.freeze({});
+
+export default function useStable(initialValueCallback: () => T): T {
+  const ref = useRef(UNINITIALIZED);
+  if (ref.current === UNINITIALIZED) {
+    ref.current = initialValueCallback();
+  }
+  return ref.current as T;
+}
diff --git a/streaming-test-app/src/vite-env.d.ts b/streaming-test-app/src/vite-env.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..11f02fe2a0061d6e6e1f271b21da95423b448b32
--- /dev/null
+++ b/streaming-test-app/src/vite-env.d.ts
@@ -0,0 +1 @@
+///