From 8c5dbb708cfb3dc651e751c834db84f2efe2c9eb Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 19:55:52 -0800 Subject: [PATCH 01/21] Setting up for deploy --- Caddyfile | 7 + cli/prod.sh | 13 + docker/.gitignore | 2 + docker/backend.Dockerfile | 16 + docker/backend.Dockerfile.dockerignore | 101 +++ docker/compose.yml | 48 ++ docker/frontend.Dockerfile | 13 + docker/frontend.Dockerfile.dockerignore | 10 + docker/playground.Dockerfile | 18 + frontend/.gitignore | 23 + frontend/.npmrc | 1 + frontend/README.md | 38 + frontend/package.json | 23 + frontend/pnpm-lock.yaml | 935 ++++++++++++++++++++++++ frontend/src/app.d.ts | 13 + frontend/src/app.html | 15 + frontend/src/routes/+layout.ts | 3 + frontend/src/routes/+page.svelte | 5 + frontend/static/favicon.png | Bin 0 -> 1571 bytes frontend/svelte.config.js | 18 + frontend/tsconfig.json | 19 + frontend/vite.config.ts | 6 + main.py | 79 +- 23 files changed, 1379 insertions(+), 27 deletions(-) create mode 100644 Caddyfile create mode 100755 cli/prod.sh create mode 100644 docker/.gitignore create mode 100644 docker/backend.Dockerfile create mode 100644 docker/backend.Dockerfile.dockerignore create mode 100644 docker/compose.yml create mode 100644 docker/frontend.Dockerfile create mode 100644 docker/frontend.Dockerfile.dockerignore create mode 100644 docker/playground.Dockerfile create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/README.md create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/src/app.d.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/routes/+layout.ts create mode 100644 frontend/src/routes/+page.svelte create mode 100644 frontend/static/favicon.png create mode 100644 frontend/svelte.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..88de925 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,7 @@ +http://doodlebot.media.mit.edu { + reverse_proxy backend:80 +} + +https://doodlebot.media.mit.edu { + reverse_proxy backend:80 +} \ No newline at end of file diff --git a/cli/prod.sh b/cli/prod.sh new file mode 100755 index 0000000..766c270 --- /dev/null +++ b/cli/prod.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status. +set -e + +# Get the directory of the Docker Compose file +ROOT_DIR="$(git rev-parse --show-toplevel)" +DOCKER_DIR="$ROOT_DIR/docker" +COMPOSE_FILE="$DOCKER_DIR/compose.yml" + +docker compose -f "$COMPOSE_FILE" up --build frontend playground + +docker compose -f "$COMPOSE_FILE" up --build backend caddy \ No newline at end of file diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..edfa78c --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,2 @@ +.frontend +.playground \ No newline at end of file diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 0000000..900c153 --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9 + +# Install dependencies for pyaudio +RUN apt-get update +RUN apt-get install libasound-dev libportaudio2 libportaudiocpp0 portaudio19-dev -y +RUN apt-get install gcc -y + +WORKDIR /app + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY . /app + +CMD ["uvicorn", "main:app", "--port", "80"] \ No newline at end of file diff --git a/docker/backend.Dockerfile.dockerignore b/docker/backend.Dockerfile.dockerignore new file mode 100644 index 0000000..a283ba9 --- /dev/null +++ b/docker/backend.Dockerfile.dockerignore @@ -0,0 +1,101 @@ +cli +docker +frontend + +# Tests +**/__tests__/ +conftest.py + +# Dev / database migrations +**/alembic* + +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +*Dockerfile* +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ \ No newline at end of file diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..95abaf6 --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,48 @@ +services: + frontend: + container_name: doodlebot-frontend + volumes: + - ./.frontend:/dist + build: + context: ../frontend + dockerfile: ../docker/frontend.Dockerfile # NOTE: path is relative to above context + restart: "no" + + playground: + container_name: doodlebot-playground + volumes: + - ./.playground:/dist + build: + dockerfile: ./playground.Dockerfile + restart: "no" + + backend: + container_name: doodlebot-backend + build: + context: .. + dockerfile: ./docker/backend.Dockerfile + ports: + - "8000:80" + volumes: + - "./.frontend:/app/frontend" + - "./.playground:/app/playground" + env_file: + - path: ../.env + required: false + networks: + - app-network + + caddy: + container_name: doodlebot-caddy + image: caddy:latest + ports: + - "80:80" + - "443:443" + volumes: + - ../Caddyfile:/etc/caddy/Caddyfile + networks: + - app-network + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile new file mode 100644 index 0000000..91ff61c --- /dev/null +++ b/docker/frontend.Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-slim AS dependencies +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +COPY package.json pnpm-lock.yaml /app/ +WORKDIR /app +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile + +FROM dependencies AS build +COPY . /app +WORKDIR /app +RUN pnpm build +CMD cp -r build/* /dist \ No newline at end of file diff --git a/docker/frontend.Dockerfile.dockerignore b/docker/frontend.Dockerfile.dockerignore new file mode 100644 index 0000000..fe6a775 --- /dev/null +++ b/docker/frontend.Dockerfile.dockerignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* \ No newline at end of file diff --git a/docker/playground.Dockerfile b/docker/playground.Dockerfile new file mode 100644 index 0000000..a98bb7a --- /dev/null +++ b/docker/playground.Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:latest AS base +RUN apt-get update && apt-get install -y git + +WORKDIR /playground + +# Clone the repo shallowly, filtering out all blobs initially, and only fetching the gh-pages branch. +# "--filter=blob:none" means blobs (file contents) are only downloaded on demand. +# "--sparse" enables sparse checkout directly from clone. +RUN git clone --depth=1 --branch=gh-pages --filter=blob:none --sparse \ + https://github.com/mitmedialab/prg-raise-playground.git /playground + +# Enable sparse checkout and select only the "curriculum" directory. +RUN git sparse-checkout set curriculum + +# After setting sparse-checkout, "git checkout" will update the working copy to include only that folder. +RUN git checkout + +CMD git pull && cp -r curriculum/* /dist \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..b5b2950 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..dfd66ab --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontend", + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^6.0.0" + }, + "dependencies": { + "@sveltejs/adapter-static": "^3.0.6" + } +} \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..b4382d6 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,935 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@sveltejs/adapter-static': + specifier: ^3.0.6 + version: 3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3)) + devDependencies: + '@sveltejs/kit': + specifier: ^2.9.0 + version: 2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3) + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.0 + version: 5.0.1(svelte@5.9.0)(vite@6.0.3) + svelte: + specifier: ^5.0.0 + version: 5.9.0 + svelte-check: + specifier: ^4.0.0 + version: 4.1.1(svelte@5.9.0)(typescript@5.7.2) + typescript: + specifier: ^5.0.0 + version: 5.7.2 + vite: + specifier: ^6.0.0 + version: 6.0.3 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + + '@rollup/rollup-android-arm-eabi@4.28.1': + resolution: {integrity: sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.28.1': + resolution: {integrity: sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.28.1': + resolution: {integrity: sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.28.1': + resolution: {integrity: sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.28.1': + resolution: {integrity: sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.28.1': + resolution: {integrity: sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.28.1': + resolution: {integrity: sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.28.1': + resolution: {integrity: sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.28.1': + resolution: {integrity: sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.28.1': + resolution: {integrity: sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.28.1': + resolution: {integrity: sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': + resolution: {integrity: sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.28.1': + resolution: {integrity: sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.28.1': + resolution: {integrity: sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.28.1': + resolution: {integrity: sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.28.1': + resolution: {integrity: sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.28.1': + resolution: {integrity: sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.28.1': + resolution: {integrity: sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.28.1': + resolution: {integrity: sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==} + cpu: [x64] + os: [win32] + + '@sveltejs/adapter-static@3.0.6': + resolution: {integrity: sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/kit@2.9.0': + resolution: {integrity: sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte@5.0.1': + resolution: {integrity: sha512-D5l5+STmywGoLST07T9mrqqFFU+xgv5fqyTWM+VbxTvQ6jujNn4h3lQNCvlwVYs4Erov8i0K5Rwr3LQtmBYmBw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + acorn-typescript@1.4.13: + resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} + peerDependencies: + acorn: '>=8.9.0' + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + + esm-env@1.2.1: + resolution: {integrity: sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==} + + esrap@1.2.3: + resolution: {integrity: sha512-ZlQmCCK+n7SGoqo7DnfKaP1sJZa49P01/dXzmjCASSo04p72w8EksT2NMK8CEX8DhKsfJXANioIw8VyHNsBfvQ==} + + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.14: + resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + + rollup@4.28.1: + resolution: {integrity: sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + svelte-check@4.1.1: + resolution: {integrity: sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte@5.9.0: + resolution: {integrity: sha512-ZcC3BtjIDa4yfhAyAr94MxDQLD97zbpXmaUldFv2F5AkdZwYgQYB3BZVNRU5zEVaeeHoAns8ADiRMnre3QmpxQ==} + engines: {node: '>=18'} + + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + vite@6.0.3: + resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.0.4: + resolution: {integrity: sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + vite: + optional: true + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@esbuild/aix-ppc64@0.24.0': + optional: true + + '@esbuild/android-arm64@0.24.0': + optional: true + + '@esbuild/android-arm@0.24.0': + optional: true + + '@esbuild/android-x64@0.24.0': + optional: true + + '@esbuild/darwin-arm64@0.24.0': + optional: true + + '@esbuild/darwin-x64@0.24.0': + optional: true + + '@esbuild/freebsd-arm64@0.24.0': + optional: true + + '@esbuild/freebsd-x64@0.24.0': + optional: true + + '@esbuild/linux-arm64@0.24.0': + optional: true + + '@esbuild/linux-arm@0.24.0': + optional: true + + '@esbuild/linux-ia32@0.24.0': + optional: true + + '@esbuild/linux-loong64@0.24.0': + optional: true + + '@esbuild/linux-mips64el@0.24.0': + optional: true + + '@esbuild/linux-ppc64@0.24.0': + optional: true + + '@esbuild/linux-riscv64@0.24.0': + optional: true + + '@esbuild/linux-s390x@0.24.0': + optional: true + + '@esbuild/linux-x64@0.24.0': + optional: true + + '@esbuild/netbsd-x64@0.24.0': + optional: true + + '@esbuild/openbsd-arm64@0.24.0': + optional: true + + '@esbuild/openbsd-x64@0.24.0': + optional: true + + '@esbuild/sunos-x64@0.24.0': + optional: true + + '@esbuild/win32-arm64@0.24.0': + optional: true + + '@esbuild/win32-ia32@0.24.0': + optional: true + + '@esbuild/win32-x64@0.24.0': + optional: true + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@polka/url@1.0.0-next.28': {} + + '@rollup/rollup-android-arm-eabi@4.28.1': + optional: true + + '@rollup/rollup-android-arm64@4.28.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.28.1': + optional: true + + '@rollup/rollup-darwin-x64@4.28.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.28.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.28.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.28.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.28.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.28.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.28.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.28.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.28.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.28.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.28.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.28.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.28.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.28.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.28.1': + optional: true + + '@sveltejs/adapter-static@3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3))': + dependencies: + '@sveltejs/kit': 2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3) + + '@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3)': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.0.1(svelte@5.9.0)(vite@6.0.3) + '@types/cookie': 0.6.0 + cookie: 0.6.0 + devalue: 5.1.1 + esm-env: 1.2.1 + import-meta-resolve: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.14 + mrmime: 2.0.0 + sade: 1.8.1 + set-cookie-parser: 2.7.1 + sirv: 3.0.0 + svelte: 5.9.0 + tiny-glob: 0.2.9 + vite: 6.0.3 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3)': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.0.1(svelte@5.9.0)(vite@6.0.3) + debug: 4.4.0 + svelte: 5.9.0 + vite: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3)': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3) + debug: 4.4.0 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.14 + svelte: 5.9.0 + vite: 6.0.3 + vitefu: 1.0.4(vite@6.0.3) + transitivePeerDependencies: + - supports-color + + '@types/cookie@0.6.0': {} + + '@types/estree@1.0.6': {} + + acorn-typescript@1.4.13(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + + cookie@0.6.0: {} + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + devalue@5.1.1: {} + + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + + esm-env@1.2.1: {} + + esrap@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + + fdir@6.4.2: {} + + fsevents@2.3.3: + optional: true + + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + + import-meta-resolve@4.1.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + kleur@4.1.5: {} + + locate-character@3.0.0: {} + + magic-string@0.30.14: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mri@1.2.0: {} + + mrmime@2.0.0: {} + + ms@2.1.3: {} + + nanoid@3.3.8: {} + + picocolors@1.1.1: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.0.2: {} + + rollup@4.28.1: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.28.1 + '@rollup/rollup-android-arm64': 4.28.1 + '@rollup/rollup-darwin-arm64': 4.28.1 + '@rollup/rollup-darwin-x64': 4.28.1 + '@rollup/rollup-freebsd-arm64': 4.28.1 + '@rollup/rollup-freebsd-x64': 4.28.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.28.1 + '@rollup/rollup-linux-arm-musleabihf': 4.28.1 + '@rollup/rollup-linux-arm64-gnu': 4.28.1 + '@rollup/rollup-linux-arm64-musl': 4.28.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.28.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.28.1 + '@rollup/rollup-linux-riscv64-gnu': 4.28.1 + '@rollup/rollup-linux-s390x-gnu': 4.28.1 + '@rollup/rollup-linux-x64-gnu': 4.28.1 + '@rollup/rollup-linux-x64-musl': 4.28.1 + '@rollup/rollup-win32-arm64-msvc': 4.28.1 + '@rollup/rollup-win32-ia32-msvc': 4.28.1 + '@rollup/rollup-win32-x64-msvc': 4.28.1 + fsevents: 2.3.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + set-cookie-parser@2.7.1: {} + + sirv@3.0.0: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + svelte-check@4.1.1(svelte@5.9.0)(typescript@5.7.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + chokidar: 4.0.1 + fdir: 6.4.2 + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.9.0 + typescript: 5.7.2 + transitivePeerDependencies: + - picomatch + + svelte@5.9.0: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + acorn: 8.14.0 + acorn-typescript: 1.4.13(acorn@8.14.0) + aria-query: 5.3.2 + axobject-query: 4.1.0 + esm-env: 1.2.1 + esrap: 1.2.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.14 + zimmerframe: 1.1.2 + + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + + totalist@3.0.1: {} + + typescript@5.7.2: {} + + vite@6.0.3: + dependencies: + esbuild: 0.24.0 + postcss: 8.4.49 + rollup: 4.28.1 + optionalDependencies: + fsevents: 2.3.3 + + vitefu@1.0.4(vite@6.0.3): + optionalDependencies: + vite: 6.0.3 + + zimmerframe@1.1.2: {} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..f8b2ff3 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + + \ No newline at end of file diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts new file mode 100644 index 0000000..4d6aa96 --- /dev/null +++ b/frontend/src/routes/+layout.ts @@ -0,0 +1,3 @@ +export const ssr = false; +export const prerender = true; +export const trailingSlash = 'always'; \ No newline at end of file diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..b5b6a03 --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,5 @@ +

Welcome to SvelteKit!

+

+ Visit svelte.dev/docs/kit to read the + documentation +

diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH bytes: """Record audio from microphone""" p = pyaudio.PyAudio() - + try: stream = p.open( format=self.FORMAT, @@ -71,12 +76,12 @@ async def record_audio(self) -> bytes: input=True, frames_per_buffer=self.CHUNK ) - + frames = [] for _ in range(0, int(self.RATE / self.CHUNK * self.RECORD_SECONDS)): data = stream.read(self.CHUNK) frames.append(data) - + temp_path = os.path.join(self.temp_dir, "temp_recording.wav") wf = wave.open(temp_path, 'wb') wf.setnchannels(self.CHANNELS) @@ -84,12 +89,12 @@ async def record_audio(self) -> bytes: wf.setframerate(self.RATE) wf.writeframes(b''.join(frames)) wf.close() - + with open(temp_path, 'rb') as audio_file: audio_bytes = audio_file.read() - + return audio_bytes - + finally: stream.stop_stream() stream.close() @@ -110,16 +115,17 @@ async def get_chat_response(self, text: str) -> str: """Get response from ChatGPT""" try: self.conversation_history.append({"role": "user", "content": text}) - + response = self.openai_client.chat.completions.create( model="gpt-4", messages=self.conversation_history, max_tokens=150 ) - + assistant_response = response.choices[0].message.content - self.conversation_history.append({"role": "assistant", "content": assistant_response}) - + self.conversation_history.append( + {"role": "assistant", "content": assistant_response}) + return assistant_response except Exception as e: raise VoiceAssistantError(f"Chat processing failed: {str(e)}") @@ -128,20 +134,21 @@ async def synthesize_speech(self, text: str) -> str: """Convert text to speech using Azure""" try: output_path = os.path.join(self.temp_dir, "response.wav") - audio_config = speechsdk.audio.AudioOutputConfig(filename=output_path) - + audio_config = speechsdk.audio.AudioOutputConfig( + filename=output_path) + synthesizer = speechsdk.SpeechSynthesizer( speech_config=self.speech_config, audio_config=audio_config ) - + result = synthesizer.speak_text_async(text).get() - + if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted: return output_path else: raise VoiceAssistantError("Speech synthesis failed") - + except Exception as e: raise VoiceAssistantError(f"Speech synthesis failed: {str(e)}") @@ -150,13 +157,13 @@ async def process_voice_input(self, audio_data: bytes = None) -> tuple[str, str] try: if audio_data is None: audio_data = await self.record_audio() - + transcript = await self.transcribe_audio(audio_data) response_text = await self.get_chat_response(transcript) audio_path = await self.synthesize_speech(response_text) - + return response_text, audio_path - + except Exception as e: raise VoiceAssistantError(f"Voice processing failed: {str(e)}") @@ -168,15 +175,18 @@ def cleanup(self): except Exception: pass + class ChatResponse(BaseModel): text: str audio_path: str -@app.get("/") + +@app.get("/health") async def root(): """Health check endpoint""" return {"status": "ok", "message": "Voice Assistant API is running"} + @app.post("/chat", response_model=ChatResponse) @handle_errors async def chat_endpoint(audio_file: UploadFile = File(None)): @@ -186,18 +196,18 @@ async def chat_endpoint(audio_file: UploadFile = File(None)): audio_data = None if audio_file: audio_data = await audio_file.read() - + response_text, audio_path = await assistant.process_voice_input(audio_data) - + with open(audio_path, 'rb') as f: audio_content = f.read() - + assistant.cleanup() - + temp_response_path = tempfile.mktemp(suffix='.wav') with open(temp_response_path, 'wb') as f: f.write(audio_content) - + return FileResponse( path=temp_response_path, media_type="audio/wav", @@ -209,6 +219,21 @@ async def chat_endpoint(audio_file: UploadFile = File(None)): assistant.cleanup() raise VoiceAssistantError(f"Chat processing failed: {str(e)}") + +def get_static_directory(name: str): + return os.path.join(os.getcwd(), name) + + +def try_mount_static_html(app, name: str, prefix: str = "/"): + directory = get_static_directory(name) + if os.path.exists(directory): + app.mount(prefix, StaticFiles( + directory=directory, html=True), name=name) + + +try_mount_static_html(app, "frontend") +try_mount_static_html(app, "playground", "/playground") + if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=8000) From 5ba5d2049588df33ae6445a768567602e7679f1e Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 20:42:40 -0800 Subject: [PATCH 02/21] detatching caddy & backend processes --- cli/prod.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/prod.sh b/cli/prod.sh index 766c270..f906673 100755 --- a/cli/prod.sh +++ b/cli/prod.sh @@ -10,4 +10,4 @@ COMPOSE_FILE="$DOCKER_DIR/compose.yml" docker compose -f "$COMPOSE_FILE" up --build frontend playground -docker compose -f "$COMPOSE_FILE" up --build backend caddy \ No newline at end of file +docker compose -f "$COMPOSE_FILE" up --build --detach backend caddy \ No newline at end of file From 70165501745e4aba63ac110ad96d16a65e8bfc26 Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 20:49:06 -0800 Subject: [PATCH 03/21] trying to correct caddy config --- Caddyfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Caddyfile b/Caddyfile index 88de925..b17f5b2 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,7 +1,7 @@ -http://doodlebot.media.mit.edu { +doodlebot.media.mit.edu { reverse_proxy backend:80 } -https://doodlebot.media.mit.edu { +http://doodlebot.media.mit.edu { reverse_proxy backend:80 } \ No newline at end of file From d5f8ab155ebe2c4043af9a0266df0952cdbfb15d Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 20:54:15 -0800 Subject: [PATCH 04/21] trying to get caddy working --- .gitignore | 2 ++ docker/backend.Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ffe490a..1ba751f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +nohup.out + # Python __pycache__/ *.py[cod] diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile index 900c153..5f8c4c7 100644 --- a/docker/backend.Dockerfile +++ b/docker/backend.Dockerfile @@ -13,4 +13,4 @@ RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt COPY . /app -CMD ["uvicorn", "main:app", "--port", "80"] \ No newline at end of file +CMD ["uvicorn", "main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file From b83941135f0f2deb4d0849a53a48c53ba7af3769 Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 21:00:15 -0800 Subject: [PATCH 05/21] logging if static dir fails to mount --- main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.py b/main.py index 6efa0e6..e4d289b 100644 --- a/main.py +++ b/main.py @@ -229,6 +229,8 @@ def try_mount_static_html(app, name: str, prefix: str = "/"): if os.path.exists(directory): app.mount(prefix, StaticFiles( directory=directory, html=True), name=name) + else: + print(f"Directory not found: {directory}") try_mount_static_html(app, "frontend") From 6c62b2498f1db319bf0e06869b16823a06d9324d Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 21:07:40 -0800 Subject: [PATCH 06/21] changing up order of static files --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index e4d289b..29f9678 100644 --- a/main.py +++ b/main.py @@ -233,8 +233,8 @@ def try_mount_static_html(app, name: str, prefix: str = "/"): print(f"Directory not found: {directory}") -try_mount_static_html(app, "frontend") try_mount_static_html(app, "playground", "/playground") +try_mount_static_html(app, "frontend") if __name__ == "__main__": import uvicorn From 8feb21b767a4197f3852aeb6738137352754143e Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 21:09:52 -0800 Subject: [PATCH 07/21] realized maybe more than one static files doesnt work --- docker/compose.yml | 2 +- main.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/compose.yml b/docker/compose.yml index 95abaf6..39b526e 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -25,7 +25,7 @@ services: - "8000:80" volumes: - "./.frontend:/app/frontend" - - "./.playground:/app/playground" + - "./.playground:/app/frontend/playground" env_file: - path: ../.env required: false diff --git a/main.py b/main.py index 29f9678..5aa3988 100644 --- a/main.py +++ b/main.py @@ -233,7 +233,6 @@ def try_mount_static_html(app, name: str, prefix: str = "/"): print(f"Directory not found: {directory}") -try_mount_static_html(app, "playground", "/playground") try_mount_static_html(app, "frontend") if __name__ == "__main__": From c86a72bb6908a184ebd35c0e99a9d5c4bdb229d7 Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 21:12:08 -0800 Subject: [PATCH 08/21] logging --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 5aa3988..3cc2f59 100644 --- a/main.py +++ b/main.py @@ -229,6 +229,7 @@ def try_mount_static_html(app, name: str, prefix: str = "/"): if os.path.exists(directory): app.mount(prefix, StaticFiles( directory=directory, html=True), name=name) + print(f"Mounted {name} at {prefix}") else: print(f"Directory not found: {directory}") From 241b37db759a6a5b9c2acf3a060e275e36b86eed Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Sun, 8 Dec 2024 21:18:29 -0800 Subject: [PATCH 09/21] adding volumes to keep track of caddy data so credentials aren't refreshed constantly --- docker/compose.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker/compose.yml b/docker/compose.yml index 39b526e..cb3d9b6 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -40,9 +40,15 @@ services: - "443:443" volumes: - ../Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config networks: - app-network networks: app-network: - driver: bridge \ No newline at end of file + driver: bridge + +volumes: + caddy_data: + caddy_config: \ No newline at end of file From 8fd4bb4141f20c92989178ff952a698816e93edc Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Mon, 9 Dec 2024 01:49:52 -0800 Subject: [PATCH 10/21] ready for testing --- DEPLOY.md | 35 +++++++++ frontend/package.json | 6 +- frontend/pnpm-lock.yaml | 88 +++++++++++++++++----- frontend/src/routes/+page.svelte | 124 +++++++++++++++++++++++++++++-- 4 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 DEPLOY.md diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..b7ad7aa --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,35 @@ +# Deployment + +The doodlebot application is currently deployed on an Ubuntu virtual machine issued to us by Necsys: [doodlebot.media.mit.edu](https://doodlebot.media.mit.edu) + +It works by executing a series of [docker containers](https://www.docker.com/resources/what-container/): + +- **backend:** This container executes the FastAPI application defined in [main.py](./main.py). The FastAPI is also responsible for serving the static assets of the **frontend** and **playground** containers described below. +- **frontend:** This container builds the [SvelteKit](https://svelte.dev/) web app defined in [frontend/](./frontend/) into a static website, which acts as the frontend you see when navigating to [doodlebot.media.mit.edu](https://doodlebot.media.mit.edu) +- **playground:** This container pulls down a single deployed folder of the [RAISE Playground repo](https://github.com/mitmedialab/prg-raise-playground) + - Currently, it pulls down the deploy of the [curriculum branch](https://github.com/mitmedialab/prg-raise-playground/tree/curriculum), which is configured in [docker/playground.Dockerfile](./docker/playground.Dockerfile) +- **caddy:** This container executes the [Caddy](https://caddyserver.com/) web server, which routes traffic to the **backend** container + +The activity of these containers is coordinated using [docker compose](https://docs.docker.com/compose/) as configured in [docker/compose.yml](./docker/compose.yml). + + +## Deployment Steps + +The following steps demonstrate how to re-deploy the backend after pushing up changes from your local development environment (you should **NOT** develop directly on this machine). + +The steps make reference to shell scripts stored directly on the deployment machine (and not in this repo, though they make use of scripts stored inside of the [cli/](./cli/) directory). These exist just to make the deployment process easier, but they often are pretty simple and you should definitely check out their contents (e.g. `cat ./example.sh`) to get a clear picture of what's going on + +1. Log onto the VM: `ssh @doodlebot.media.mit.edu` + - Use the `user_id` @pmalacho provided to you as well as the password when prompted +2. Create a sudo session: `sudo su` + - You'll again be prompted for your password +3. Pull the latest changes for this repo: `./pull.sh` +4. (Re)start the docker containers: `./start.sh` + - **NOTE:** If you deem it necessary, you can explicitly stop all currently running containers first: `./stop.sh` +5. When you run the above `./start.sh` command, you should see `nohup: ignoring input and appending output to 'nohup.out'` + - This message indicates that we are running a process (specifically [cli/prod.sh](./cli/prod.sh)) through the [nohup](https://www.digitalocean.com/community/tutorials/nohup-command-in-linux) utility. It stands for "No Hang Up", which is useful for us so that the deployment process continues even when we exit the terminal (aka "hang up"). + - This command won't exit until all of the docker containers are built. If you need to see the output of this build process, you can open another terminal, ssh onto the machine, start a sudo session (`sudo su`) and run `./log-build.sh` (which literally just prints the contents of the `'nohup.out'` file referenced above). + - As you can see, it's sometimes helpful to have two terminals connected to the deployment machine, especially when debugging. +6. Once the `./start.sh` command exits, the server will be starting up. During this time the site ([doodlebot.media.mit.edu](https://doodlebot.media.mit.edu)) will not be reachable -- in this sense, we do **NOT** currently support [zero downtime deployments](https://www.pingidentity.com/en/resources/blog/post/what-is-zero-downtime-deployment.html). + - To see the output of the server at runtime, run `./log-runtime.sh`. + - Once you see `doodlebot-backend | Mounted frontend at /` in the output of `./log-runtime.sh`, the full site should be responsive. \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index dfd66ab..08f3176 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,12 +12,16 @@ "devDependencies": { "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/events": "^3.0.3", + "@types/web-bluetooth": "^0.0.20", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", "vite": "^6.0.0" }, "dependencies": { - "@sveltejs/adapter-static": "^3.0.6" + "@sveltejs/adapter-static": "^3.0.6", + "events": "^3.3.0", + "microbit-web-bluetooth": "^0.7.0" } } \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b4382d6..662ede4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,14 +10,26 @@ importers: dependencies: '@sveltejs/adapter-static': specifier: ^3.0.6 - version: 3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3)) + version: 3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9))) + events: + specifier: ^3.3.0 + version: 3.3.0 + microbit-web-bluetooth: + specifier: ^0.7.0 + version: 0.7.0 devDependencies: '@sveltejs/kit': specifier: ^2.9.0 - version: 2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3) + version: 2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.0.1(svelte@5.9.0)(vite@6.0.3) + version: 5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)) + '@types/events': + specifier: ^3.0.3 + version: 3.0.3 + '@types/web-bluetooth': + specifier: ^0.0.20 + version: 0.0.20 svelte: specifier: ^5.0.0 version: 5.9.0 @@ -29,7 +41,7 @@ importers: version: 5.7.2 vite: specifier: ^6.0.0 - version: 6.0.3 + version: 6.0.3(@types/node@20.17.9) packages: @@ -332,6 +344,15 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/events@3.0.3': + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + + '@types/node@20.17.9': + resolution: {integrity: sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + acorn-typescript@1.4.13: resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} peerDependencies: @@ -385,6 +406,10 @@ packages: esrap@1.2.3: resolution: {integrity: sha512-ZlQmCCK+n7SGoqo7DnfKaP1sJZa49P01/dXzmjCASSo04p72w8EksT2NMK8CEX8DhKsfJXANioIw8VyHNsBfvQ==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + fdir@6.4.2: resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} peerDependencies: @@ -420,6 +445,10 @@ packages: magic-string@0.30.14: resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} + microbit-web-bluetooth@0.7.0: + resolution: {integrity: sha512-CvJnQZJb/QOQfpNHiMRrLP+Djtodn5kmHKMn0OhbwYWXpFwcSIOzbyK5Jfdv8xnqFG7n4YUtOJtR0WdE3ql6cQ==} + engines: {node: '>=10.16.0'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -491,6 +520,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + vite@6.0.3: resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -697,13 +729,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.28.1': optional: true - '@sveltejs/adapter-static@3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3))': + '@sveltejs/adapter-static@3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))': dependencies: - '@sveltejs/kit': 2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3) + '@sveltejs/kit': 2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)) - '@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3)': + '@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.1(svelte@5.9.0)(vite@6.0.3) + '@sveltejs/vite-plugin-svelte': 5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -717,27 +749,27 @@ snapshots: sirv: 3.0.0 svelte: 5.9.0 tiny-glob: 0.2.9 - vite: 6.0.3 + vite: 6.0.3(@types/node@20.17.9) - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3)': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.1(svelte@5.9.0)(vite@6.0.3) + '@sveltejs/vite-plugin-svelte': 5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)) debug: 4.4.0 svelte: 5.9.0 - vite: 6.0.3 + vite: 6.0.3(@types/node@20.17.9) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3)': + '@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3))(svelte@5.9.0)(vite@6.0.3) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.1(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)))(svelte@5.9.0)(vite@6.0.3(@types/node@20.17.9)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.14 svelte: 5.9.0 - vite: 6.0.3 - vitefu: 1.0.4(vite@6.0.3) + vite: 6.0.3(@types/node@20.17.9) + vitefu: 1.0.4(vite@6.0.3(@types/node@20.17.9)) transitivePeerDependencies: - supports-color @@ -745,6 +777,14 @@ snapshots: '@types/estree@1.0.6': {} + '@types/events@3.0.3': {} + + '@types/node@20.17.9': + dependencies: + undici-types: 6.19.8 + + '@types/web-bluetooth@0.0.20': {} + acorn-typescript@1.4.13(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -803,6 +843,8 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@types/estree': 1.0.6 + events@3.3.0: {} + fdir@6.4.2: {} fsevents@2.3.3: @@ -826,6 +868,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + microbit-web-bluetooth@0.7.0: + dependencies: + '@types/node': 20.17.9 + '@types/web-bluetooth': 0.0.20 + mri@1.2.0: {} mrmime@2.0.0: {} @@ -920,16 +967,19 @@ snapshots: typescript@5.7.2: {} - vite@6.0.3: + undici-types@6.19.8: {} + + vite@6.0.3(@types/node@20.17.9): dependencies: esbuild: 0.24.0 postcss: 8.4.49 rollup: 4.28.1 optionalDependencies: + '@types/node': 20.17.9 fsevents: 2.3.3 - vitefu@1.0.4(vite@6.0.3): + vitefu@1.0.4(vite@6.0.3(@types/node@20.17.9)): optionalDependencies: - vite: 6.0.3 + vite: 6.0.3(@types/node@20.17.9) zimmerframe@1.1.2: {} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index b5b6a03..040bad8 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,5 +1,119 @@ -

Welcome to SvelteKit!

-

- Visit svelte.dev/docs/kit to read the - documentation -

+ + + + + + + + + + + + From 18fb0118ec5a2100b1c2c27a69be9cd322323d1a Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Mon, 9 Dec 2024 02:06:04 -0800 Subject: [PATCH 11/21] temporary change for let's encrypt staging server --- Caddyfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Caddyfile b/Caddyfile index b17f5b2..9e31cf6 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,3 +1,7 @@ +{ + acme_ca https://acme-staging-v02.api.letsencrypt.org/directory +} + doodlebot.media.mit.edu { reverse_proxy backend:80 } From fa6934dc29c099acdc7f47b4d33f4415a35e6c4a Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Mon, 9 Dec 2024 11:53:05 -0800 Subject: [PATCH 12/21] removing lib from root gitingore so fronte/src/lib files actually get added --- .gitignore | 1 - .../src/lib/communication/EventDispatcher.ts | 57 +++++++++ .../src/lib/communication/PromiseQueue.ts | 70 +++++++++++ .../src/lib/communication/ServiceHelper.ts | 80 +++++++++++++ frontend/src/lib/communication/UartService.ts | 111 ++++++++++++++++++ frontend/src/lib/index.ts | 1 + 6 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/communication/EventDispatcher.ts create mode 100644 frontend/src/lib/communication/PromiseQueue.ts create mode 100644 frontend/src/lib/communication/ServiceHelper.ts create mode 100644 frontend/src/lib/communication/UartService.ts create mode 100644 frontend/src/lib/index.ts diff --git a/.gitignore b/.gitignore index 1ba751f..4c70900 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/frontend/src/lib/communication/EventDispatcher.ts b/frontend/src/lib/communication/EventDispatcher.ts new file mode 100644 index 0000000..92a7b8e --- /dev/null +++ b/frontend/src/lib/communication/EventDispatcher.ts @@ -0,0 +1,57 @@ +/* + * micro:bit Web Bluetooth + * Copyright (c) 2019 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { EventEmitter } from "events"; + +export default class EventDispatcher = Record> extends EventEmitter { + isEventListenerObject(listener: any): listener is EventListenerObject { + return listener.handleEvent !== undefined; + } + + addEventListener(type: K, listener: (event: CustomEvent) => void): void { + if (listener) { + const handler = this.isEventListenerObject(listener) + ? listener.handleEvent + : listener; + super.addListener(type, handler); + } + } + + removeEventListener(type: K, callback: (event: CustomEvent) => void): void { + if (callback) { + const handler = this.isEventListenerObject(callback) + ? callback.handleEvent + : callback; + super.removeListener(type, handler); + } + } + + dispatchEvent>(eventOrType: K, detail: K extends string ? T[K] : K): boolean { + const event = typeof eventOrType === "string" + ? new CustomEvent(eventOrType, { detail, }) + : eventOrType as CustomEvent; + return super.emit(event.type, event); + } +} \ No newline at end of file diff --git a/frontend/src/lib/communication/PromiseQueue.ts b/frontend/src/lib/communication/PromiseQueue.ts new file mode 100644 index 0000000..fb2b3e2 --- /dev/null +++ b/frontend/src/lib/communication/PromiseQueue.ts @@ -0,0 +1,70 @@ +/* + * micro:bit Web Bluetooth + * Copyright (c) 2019 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +type QueuedPromise = { + fn: () => Promise; + resolve: (value?: any | PromiseLike | undefined) => void; + reject: (reason?: any) => void; +} + +export default class PromiseQueue { + private queue: QueuedPromise[] = []; + private running: number = 0; + + constructor(private concurrent = 1) { } + + async pump(): Promise { + if (this.running >= this.concurrent) return; + + const promise = this.queue.shift(); + + if (!promise) return; + + this.running++; + + try { + const result = await promise.fn(); + promise.resolve(result); + } catch (error) { + promise.reject(error); + } + + this.running--; + + return this.pump(); + } + + add(fn: QueuedPromise["fn"]) { + return new Promise((resolve, reject) => { + this.queue.push({ + fn, + resolve, + reject, + }); + + return this.pump(); + }); + } +} \ No newline at end of file diff --git a/frontend/src/lib/communication/ServiceHelper.ts b/frontend/src/lib/communication/ServiceHelper.ts new file mode 100644 index 0000000..402b90e --- /dev/null +++ b/frontend/src/lib/communication/ServiceHelper.ts @@ -0,0 +1,80 @@ +/* + * micro:bit Web Bluetooth + * Copyright (c) 2019 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/// + +import type { EventEmitter } from "events"; +import PromiseQueue from "./PromiseQueue"; + +export interface Service { + uuid: BluetoothCharacteristicUUID; + create(service: BluetoothRemoteGATTService): Promise; +} + +export default class ServiceHelper { + private queue = new PromiseQueue(); + private characteristics?: BluetoothRemoteGATTCharacteristic[]; + + constructor(private service: BluetoothRemoteGATTService, private emitter: EventEmitter) { } + + async getCharacteristic(uuid: string) { + this.characteristics ??= await this.service.getCharacteristics(); + return this.characteristics.find((characteristic) => characteristic.uuid === uuid); + } + + async getCharacteristicValue(uuid: string) { + const characteristic = await this.getCharacteristic(uuid); + if (!characteristic) throw new Error("Unable to locate characteristic"); + return await this.queue.add(async () => characteristic.readValue()); + } + + async setCharacteristicValue(uuid: string, value: BufferSource) { + const characteristic = await this.getCharacteristic(uuid); + if (!characteristic) throw new Error("Unable to locate characteristic"); + await this.queue.add(async () => characteristic.writeValueWithoutResponse(value)); + } + + async handleListener(event: any, uuid: string, handler: (event: Event) => void) { + const characteristic = await this.getCharacteristic(uuid); + + if (!characteristic) return; + + await this.queue.add(async () => characteristic.startNotifications()); + + this.emitter.on("newListener", (emitterEvent: any) => { + if (emitterEvent !== event || this.emitter.listenerCount(event) > 0) return; + return this.queue.add(async () => + characteristic.addEventListener("characteristicvaluechanged", handler) + ); + }); + + this.emitter.on("removeListener", (emitterEvent: any) => { + if (emitterEvent !== event || this.emitter.listenerCount(event) > 0) return; + return this.queue.add(async () => + characteristic.removeEventListener("characteristicvaluechanged", handler) + ); + }); + } +} \ No newline at end of file diff --git a/frontend/src/lib/communication/UartService.ts b/frontend/src/lib/communication/UartService.ts new file mode 100644 index 0000000..9fe02fe --- /dev/null +++ b/frontend/src/lib/communication/UartService.ts @@ -0,0 +1,111 @@ +/* + * micro:bit Web Bluetooth + * Copyright (c) 2019 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import EventDispatcher from "./EventDispatcher"; +import ServiceHelper from "./ServiceHelper"; + +/** + * Events raised by the UART service + */ +export interface UartEvents { + /** + * @hidden + */ + newListener: keyof UartEvents; + /** + * @hidden + */ + removeListener: keyof UartEvents; + /** + * Serial data received event + */ + receive: Uint8Array; + /** + * Serial received text event + */ + receiveText: string; +} + +export default class UartService extends EventDispatcher { + static readonly uuid = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; + + static readonly rx_uuid = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; + + static readonly tx_uuid = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + + static async create(service: BluetoothRemoteGATTService) { + const bluetoothService = new UartService(service); + await bluetoothService.init(); + return bluetoothService; + } + + private helper: ServiceHelper; + + constructor(service: BluetoothRemoteGATTService) { + super(); + this.helper = new ServiceHelper(service, this); + } + + async init() { + const { tx_uuid } = UartService; + await this.helper.handleListener("receive", tx_uuid, this.receiveHandler.bind(this)); + await this.helper.handleListener("receiveText", tx_uuid, this.receiveTextHandler.bind(this)); + } + + /** + * Send serial data + * @param value The buffer to send + */ + async send(value: BufferSource) { + return this.helper.setCharacteristicValue(UartService.rx_uuid, value); + } + + /** + * Send serial text + * @param value The text to send + */ + async sendText(value: string) { + console.log("sending text", value); + const arrayData = value.split("").map((e) => e.charCodeAt(0)); + return this.helper.setCharacteristicValue( + UartService.rx_uuid, + new Uint8Array(arrayData).buffer + ); + } + + receiveHandler(event: any) { + const view = event.target.value; + const value = new Uint8Array(view.buffer); + this.dispatchEvent("receive", value); + } + + receiveTextHandler(event: any) { + const view = event.target.value; + const numberArray = new Uint8Array(view.buffer).slice() + const value = String.fromCharCode.apply(null, numberArray as any); + console.log("received text", value); + this.dispatchEvent("receiveText", value); + } +} \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. From c3dcb7845dbb36eb12cd9dcee8332147ee07ccae Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Mon, 9 Dec 2024 23:50:35 -0800 Subject: [PATCH 13/21] got implementation working across two computers --- frontend/src/routes/+page.svelte | 109 ++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 30 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 040bad8..a1608ff 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -3,13 +3,25 @@ const { bluetooth } = window.navigator; - const popupBaseURL = "http://doodlebot.media.mit.edu/playground"; + const constants = { + handshakeMessage: "doodlebot", + disconnectMessage: "disconnected", + commandCompleteIdentifier: "done", + }; let popup: Window | null = null; - let password: string; - let ssid: string; - let ip: string; + let password: string = localStorage.getItem("password") || ""; + let ssid: string = localStorage.getItem("ssid") || ""; + let ip: string = localStorage.getItem("ip") || ""; + let playgroundURL: string = localStorage.getItem("playgroundURL") || ""; + + $: if (password) localStorage.setItem("password", password); + $: if (ssid) localStorage.setItem("ssid", ssid); + $: if (ip) localStorage.setItem("ip", ip); + $: if (playgroundURL) localStorage.setItem("playgroundURL", playgroundURL); + + $: popupOrigin = popup ? new URL(playgroundURL).origin : ""; let unsubscribers = new Array<() => void>(); @@ -18,10 +30,14 @@ unsubscribers = []; }; - const forwardToPopup = ({ detail }: Pick, "detail">) => - popup?.postMessage(detail, popupBaseURL); + const forwardToPopup = ({ + detail, + }: Pick, "detail">) => { + popup?.postMessage(detail, playgroundURL); + }; - const disconnect = () => forwardToPopup({ detail: "disconnect" }); + const disconnect = () => + forwardToPopup({ detail: constants.disconnectMessage }); const getUartService = async ( service: BluetoothRemoteGATTService, @@ -45,18 +61,19 @@ const waitForPopupToRespond = () => new Promise((resolve) => { let interval = setInterval( - () => popup!.postMessage("check", popupBaseURL), + () => + popup!.postMessage( + constants.handshakeMessage, + playgroundURL, + ), 100, ); const onReady = (event: MessageEvent) => { - if (!event.origin.startsWith(popupBaseURL)) return; - - if (event.data === "ready") { - clearInterval(interval!); - window.removeEventListener("message", onReady); - resolve(); - } + if (event.origin !== popupOrigin) return; + clearInterval(interval!); + window.removeEventListener("message", onReady); + resolve(); }; window.addEventListener("message", onReady); @@ -86,18 +103,24 @@ const uartService = await getUartService(found, device); - popup = window.open( - `${popupBaseURL}?password=${password}&ssid=${ssid}&ip=${ip}`, - ); + // handle case if playgroundURL ends in question mark (?) + const url = new URL(playgroundURL); + url.searchParams.set("password", password); + url.searchParams.set("ssid", ssid); + url.searchParams.set("ip", ip); + + popup = window.open(url.toString()); if (!popup) return alert("Please allow popups for this website"); await waitForPopupToRespond(); const forwardToBLE = async ({ data, origin }: MessageEvent) => { - if (!origin.startsWith(popupBaseURL)) return; + if (origin !== popupOrigin) return; await uartService.sendText(data); - forwardToPopup({ detail: data }); + forwardToPopup({ + detail: data + constants.commandCompleteIdentifier, + }); }; window.addEventListener("message", forwardToBLE); @@ -107,13 +130,39 @@ }; - - - - - - - - - - +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + From ff36b0464789225ab8eb60610481ed1373b05e37 Mon Sep 17 00:00:00 2001 From: "Github Action (authored by pmalacho-mit)" Date: Tue, 10 Dec 2024 01:17:39 -0800 Subject: [PATCH 14/21] removing wifi credentials --- DEPLOY.md | 1 + frontend/src/routes/+page.svelte | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index b7ad7aa..1f4b513 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -23,6 +23,7 @@ The steps make reference to shell scripts stored directly on the deployment mach - Use the `user_id` @pmalacho provided to you as well as the password when prompted 2. Create a sudo session: `sudo su` - You'll again be prompted for your password +3. Change directory to home: `cd` 3. Pull the latest changes for this repo: `./pull.sh` 4. (Re)start the docker containers: `./start.sh` - **NOTE:** If you deem it necessary, you can explicitly stop all currently running containers first: `./stop.sh` diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index a1608ff..9c1d034 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -11,13 +11,9 @@ let popup: Window | null = null; - let password: string = localStorage.getItem("password") || ""; - let ssid: string = localStorage.getItem("ssid") || ""; let ip: string = localStorage.getItem("ip") || ""; let playgroundURL: string = localStorage.getItem("playgroundURL") || ""; - $: if (password) localStorage.setItem("password", password); - $: if (ssid) localStorage.setItem("ssid", ssid); $: if (ip) localStorage.setItem("ip", ip); $: if (playgroundURL) localStorage.setItem("playgroundURL", playgroundURL); @@ -105,8 +101,6 @@ // handle case if playgroundURL ends in question mark (?) const url = new URL(playgroundURL); - url.searchParams.set("password", password); - url.searchParams.set("ssid", ssid); url.searchParams.set("ip", ip); popup = window.open(url.toString()); @@ -130,16 +124,6 @@ }; -
- - -
- -
- - -
-
From c17bb4216a9830d0efd32ad07fa930bce8db1f94 Mon Sep 17 00:00:00 2001 From: Brandon Lei Date: Thu, 30 Jan 2025 16:49:20 -0500 Subject: [PATCH 15/21] update * cors --- main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.py b/main.py index 3cc2f59..00cf998 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,7 @@ import os from dotenv import load_dotenv from functools import wraps +from fastapi.middleware.cors import CORSMiddleware # Load environment variables load_dotenv() @@ -21,6 +22,13 @@ description="A voice assistant that converts speech to text, processes it, and returns synthesized speech", version="1.0.0" ) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers +) # Initialize API clients openai_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) From b51a43ef0c2811e9b6af3e55be061f3bb4854c06 Mon Sep 17 00:00:00 2001 From: Brandon Lei Date: Wed, 5 Feb 2025 15:26:53 -0500 Subject: [PATCH 16/21] added cors --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 00cf998..9c88a36 100644 --- a/main.py +++ b/main.py @@ -24,9 +24,9 @@ ) app.add_middleware( CORSMiddleware, - allow_origins=["*"], # Allows all origins + allow_origins=["http://localhost:8602", "http://doodlebot.media.mit.edu"], # Allows all origins allow_credentials=True, - allow_methods=["*"], # Allows all methods + allow_methods=["GET", "POST"], # Allows all methods allow_headers=["*"], # Allows all headers ) From e093ba5f2eeaba7c586f0f6eb56ae09107bb2c83 Mon Sep 17 00:00:00 2001 From: Brandon Lei Date: Wed, 5 Feb 2025 16:26:46 -0500 Subject: [PATCH 17/21] change voice --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 9c88a36..4f626cb 100644 --- a/main.py +++ b/main.py @@ -144,7 +144,7 @@ async def synthesize_speech(self, text: str) -> str: output_path = os.path.join(self.temp_dir, "response.wav") audio_config = speechsdk.audio.AudioOutputConfig( filename=output_path) - + self.speech_config.speech_synthesis_voice_name = "en-US-AnaNeural" synthesizer = speechsdk.SpeechSynthesizer( speech_config=self.speech_config, audio_config=audio_config From b1f1e5cb876745a85ee24051c1f0cbae9c3001c3 Mon Sep 17 00:00:00 2001 From: Brandon Lei Date: Wed, 5 Feb 2025 16:30:21 -0500 Subject: [PATCH 18/21] change voice --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 4f626cb..862bafd 100644 --- a/main.py +++ b/main.py @@ -144,7 +144,7 @@ async def synthesize_speech(self, text: str) -> str: output_path = os.path.join(self.temp_dir, "response.wav") audio_config = speechsdk.audio.AudioOutputConfig( filename=output_path) - self.speech_config.speech_synthesis_voice_name = "en-US-AnaNeural" + self.speech_config.speech_synthesis_voice_name = "en-US-GuyNeural" synthesizer = speechsdk.SpeechSynthesizer( speech_config=self.speech_config, audio_config=audio_config From 811be35e6f0b12ae137d78b4957799d19148cc71 Mon Sep 17 00:00:00 2001 From: Brandon Lei Date: Wed, 5 Feb 2025 16:33:41 -0500 Subject: [PATCH 19/21] change voice --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 862bafd..d142225 100644 --- a/main.py +++ b/main.py @@ -62,7 +62,8 @@ def __init__(self): self.openai_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) self.speech_config = speechsdk.SpeechConfig( subscription=azure_speech_key, - region=azure_service_region + region=azure_service_region, + speech_synthesis_voice_name="en-US-GuyNeural" ) # Audio recording config @@ -144,7 +145,6 @@ async def synthesize_speech(self, text: str) -> str: output_path = os.path.join(self.temp_dir, "response.wav") audio_config = speechsdk.audio.AudioOutputConfig( filename=output_path) - self.speech_config.speech_synthesis_voice_name = "en-US-GuyNeural" synthesizer = speechsdk.SpeechSynthesizer( speech_config=self.speech_config, audio_config=audio_config From 87529fa08d270868c2a9803e8938466ed7459307 Mon Sep 17 00:00:00 2001 From: Brandon Lei Date: Wed, 5 Feb 2025 16:35:40 -0500 Subject: [PATCH 20/21] update prompting --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index d142225..846ca06 100644 --- a/main.py +++ b/main.py @@ -123,7 +123,7 @@ async def transcribe_audio(self, audio_bytes: bytes) -> str: async def get_chat_response(self, text: str) -> str: """Get response from ChatGPT""" try: - self.conversation_history.append({"role": "user", "content": text}) + self.conversation_history.append({"role": "user", "content": "start your response with hi student! " + text}) response = self.openai_client.chat.completions.create( model="gpt-4", From 15f730d09be2c858852ccb0800a7739c1ad820f6 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 6 Feb 2025 07:29:20 +0000 Subject: [PATCH 21/21] ensuring latest version of corepack to avoid key signing issue --- docker/frontend.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile index 91ff61c..1383446 100644 --- a/docker/frontend.Dockerfile +++ b/docker/frontend.Dockerfile @@ -1,7 +1,7 @@ FROM node:20-slim AS dependencies ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable +RUN corepack enable && npm install -g corepack@latest COPY package.json pnpm-lock.yaml /app/ WORKDIR /app RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -10,4 +10,4 @@ FROM dependencies AS build COPY . /app WORKDIR /app RUN pnpm build -CMD cp -r build/* /dist \ No newline at end of file +CMD cp -r build/* /dist