Compare commits

...

13 Commits

Author SHA1 Message Date
briquet 2ac457c43e 🔖(minor) bump release to 1.34.0 2026-10-07 12:50:40 +02:00
lebaudantoine e9b523b522 📝(docs) document Brevo marketing migration in UPGRADE.md
Explain the switch to django-lasuite marketing: removed BREVO_*
and MARKETING_SERVICE_CLASS settings, new LASUITE_MARKETING_BACKEND
and LASUITE_MARKETING_PARAMETERS variables, dummy backend default,
and the now-required Celery worker.
2026-10-07 12:16:26 +02:00
lebaudantoine 70d348337e 📝(docs) bump UPGRADE.md after v1.33 release
The `Unreleased` section of `UPGRADE.md` was not bumped when v1.33
was released, so follow-up changes landed under the wrong heading.

Reorganize the file so v1.33 is properly closed and a fresh
`Unreleased` section is opened on top.
2026-10-07 12:16:26 +02:00
lebaudantoine 258722adfd ⏪️(docs) revert styling on heartbeat and lbheartbeat probe in CHANGELOG
Revert the formatting changes applied to the heartbeat and
lbheartbeat probe entries in the CHANGELOG, as the previous styling
was intentional.
2026-10-07 11:49:23 +02:00
lebaudantoine 0a6724e7ad 🔧(compose) make the LiveKit agents opt-in in the local dev stack
`make run` no longer starts the metadata collector and the
multi-user transcriber. On my machine, this saves about 400 MB
of RAM and almost 2% CPU, out of the 11% the stack uses.
Start them with `make run-agents` when needed.

Disable `METADATA_COLLECTOR_ENABLED` by default in `common.dist` so
the backend does not dispatch jobs to an agent that is not running,
and document how to enable each agent in `developping_locally.md`.
2026-10-06 22:56:57 +02:00
lebaudantoine c1c2d93932 🔧(compose) share the backend redis with the summary stack
The summary settings already default to the `redis` host, so
`redis-summary` was unused. Remove it and depend on `redis`.

Pin the summary Celery and task tracker URLs to DB 2 in
`summary.dist` to isolate them from LiveKit and the backend
Celery broker (DB 0) and the Django cache (DB 1).
2026-10-06 17:26:26 +02:00
lebaudantoine 9efb9577c4 ♻️(tilt) use the helm dev-backend chart
Use the shared helm `dev-backend` chart to deploy the backend in
the Tilt dev stack, instead of maintaining our own copy.

The goal is to share more common pieces of the dev experience and
dev stack across projects, so we do not maintain N different
versions of similar setups.

The only adjustment needed was to backport Garage into the shared
chart.

Originally started by @rouja.
2026-10-06 14:07:23 +02:00
Ovgodd 2137c6b444 🐛(frontend) let panel shortcuts close panels opened from a menu
allow menu-invoked panels to be closed by panel shortcuts
2026-10-05 15:11:04 +02:00
Ovgodd aa01733f6b ♿️(frontend) make participant pagination readable and keyboard reachable
Improves PaginationControl: clearer structure, keyboard nav, a11y improved.
The main room and the picture-in-picture window share this control.
Ctrl+Shift+G focuses the pagination.
2026-10-05 15:11:04 +02:00
lebaudantoine 03db669e51 🔒️(ci) set persist-credentials: false on actions/checkout
By default, `actions/checkout` saves the job's auth token
(`GITHUB_TOKEN` or the provided PAT) in the local git config so
later steps can run authenticated git commands. That token then
stays on disk for the rest of the job, where it can leak:

* If an artifact upload includes the checkout directory, the token
  is packaged with it and anyone with artifact access can extract
  it. On public repos that is anyone, and the token can be used
  while the job is still running ("ArtiPACKED", flagged by
  `zizmor` as `artipacked`).
* Any later step, third-party action, or build dependency can read
  the token from the git config, which widens the impact of a
  supply-chain compromise.

None of our workflows need authenticated git after checkout, so
disable credential persistence. If a step needs to push in
2026-10-05 12:41:38 +02:00
davd-gzl 97a73bf4f2 🔧(frontend) rename the make target to test-frontend
The frontend test target now sits beside test-back and
test-summary and follows their naming.
2026-10-05 12:41:38 +02:00
davd-gzl 267a2265d9 🔧(frontend) give vitest its own config and a make target
A separate vitest.config.ts keeps the tests off the build
plugins and the mediapipe version check in vite.config.ts.
make test now runs the frontend tests after the backend ones,
and test-front runs on Node 24, the current LTS.
2026-10-05 12:41:38 +02:00
davd-gzl 14507d57a9 ✅(frontend) add vitest so the frontend can carry unit tests
Add vitest as a dev dependency, a test script that runs panda
codegen first, and a test-front job, so the frontend can carry
unit tests. One test covers normalizeRoomId, a plain function,
so no DOM library comes with it.
2026-10-05 12:41:38 +02:00
48 changed files with 730 additions and 796 deletions
+31
View File
@@ -88,6 +88,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Create writable /data - name: Create writable /data
run: | run: |
@@ -159,6 +161,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Install ffmpeg - name: Install ffmpeg
run: | run: |
@@ -186,6 +190,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Install dependencies - name: Install dependencies
run: cd src/frontend/ && npm ci --ignore-scripts run: cd src/frontend/ && npm ci --ignore-scripts
@@ -196,6 +202,27 @@ jobs:
- name: Check format - name: Check format
run: cd src/frontend/ && npm run check run: cd src/frontend/ && npm run check
test-front:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "24"
- name: Install dependencies
run: cd src/frontend/ && npm ci --ignore-scripts
- name: Run tests
run: cd src/frontend/ && npm test
lint-sdk: lint-sdk:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -206,6 +233,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Install dependencies - name: Install dependencies
run: npm ci --ignore-scripts run: npm ci --ignore-scripts
@@ -227,6 +256,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Install dependencies - name: Install dependencies
run: npm ci --ignore-scripts run: npm ci --ignore-scripts
+5
View File
@@ -8,6 +8,8 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
## [1.34.0] - 2026-10-07
### Added ### Added
- ✨(helm) import environment variables from Secrets and ConfigMaps - ✨(helm) import environment variables from Secrets and ConfigMaps
@@ -16,6 +18,8 @@ and this project adheres to
- 🔧(summary) add setting to control Sentry traces sampling rate - 🔧(summary) add setting to control Sentry traces sampling rate
- ✨(frontend) let signed-out visitors start a meeting - ✨(frontend) let signed-out visitors start a meeting
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration - ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
- ✅(frontend) add vitest so the frontend can carry unit tests
- ♿️(frontend) make participant pagination readable and keyboard reachable #1775
### Changed ### Changed
@@ -165,6 +169,7 @@ and this project adheres to
### Added ### Added
- ✨(any) let any authenticated user manage the lobby on trusted rooms - ✨(any) let any authenticated user manage the lobby on trusted rooms
### Changed ### Changed
- 📱(frontend) collapse mobile control bar items on narrow viewports - 📱(frontend) collapse mobile control bar items on narrow viewports
+7 -3
View File
@@ -169,7 +169,7 @@ run-summary: ## start only the summary application and all needed services
@$(COMPOSE) up --force-recreate -d celery-summary-summarize @$(COMPOSE) up --force-recreate -d celery-summary-summarize
.PHONY: run-summary .PHONY: run-summary
run-agents: ## start the multi-user-transcriber agent run-agents: ## start the LiveKit agents (opt-in, see docs/developping_locally.md)
@$(MAKE) run-agent-multi-user-transcriber @$(MAKE) run-agent-multi-user-transcriber
@$(MAKE) run-agent-metadata-collector @$(MAKE) run-agent-metadata-collector
.PHONY: run-agents .PHONY: run-agents
@@ -186,7 +186,6 @@ run:
run: ## start the wsgi (production) and development server run: ## start the wsgi (production) and development server
@$(MAKE) run-backend @$(MAKE) run-backend
@$(MAKE) run-summary @$(MAKE) run-summary
@$(MAKE) run-agents
@$(COMPOSE) up --force-recreate -d frontend @$(COMPOSE) up --force-recreate -d frontend
.PHONY: run .PHONY: run
@@ -260,7 +259,8 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
test: ## run project tests; pass extra pytest args via ARGS, e.g. `make test ARGS="-vv"` test: ## run project tests; pass extra pytest args via ARGS, e.g. `make test ARGS="-vv"`
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \ @args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
$(MAKE) test-back-parallel ARGS="$${args}" && \ $(MAKE) test-back-parallel ARGS="$${args}" && \
$(MAKE) test-summary ARGS="$${args}" $(MAKE) test-summary ARGS="$${args}" && \
$(MAKE) test-frontend
.PHONY: test .PHONY: test
test-back: ## run back-end tests (pass extra pytest args via ARGS) test-back: ## run back-end tests (pass extra pytest args via ARGS)
@@ -278,6 +278,10 @@ test-summary: ## run summary tests (pass extra pytest args via ARGS)
bin/pytest-summary $${args} bin/pytest-summary $${args}
.PHONY: test-summary .PHONY: test-summary
test-frontend: ## run the frontend unit tests
cd $(PATH_FRONT) && npm test
.PHONY: test-frontend
makemigrations: ## run django makemigrations for the Meet project. makemigrations: ## run django makemigrations for the Meet project.
@echo "$(BOLD)Running makemigrations$(RESET)" @echo "$(BOLD)Running makemigrations$(RESET)"
@$(COMPOSE) up -d postgresql @$(COMPOSE) up -d postgresql
+76 -25
View File
@@ -16,43 +16,54 @@ the following command inside your docker container:
## [Unreleased] ## [Unreleased]
### Purging inactive rooms ### Marketing / Brevo integration now uses `django-lasuite`
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md). The in-house marketing service (`core.services.marketing`) has been removed and
replaced by the shared implementation from `django-lasuite`
(`lasuite.marketing`). This fixes a bug where updating a user's contact on
Brevo overwrote their list memberships, removing lists set by other
La Suite products. Existing lists are now preserved and merged.
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`. **Celery worker required.** Newsletter signup on login
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading. (`SIGNUP_NEW_USER_TO_MARKETING_EMAIL=True`) is now dispatched as an
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window. asynchronous Celery task (`lasuite.marketing.tasks.create_or_update_contact`)
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged. instead of a synchronous call with a 1s timeout. Make sure a Celery worker is
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL. running alongside the backend, otherwise contacts will never be pushed to Brevo.
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights. **Configuration changes.** The following environment variables / settings are
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database. **removed** and no longer read:
### Local development: MinIO replaced by Garage - `MARKETING_SERVICE_CLASS`
- `BREVO_API_KEY`
- `BREVO_API_CONTACT_LIST_IDS`
- `BREVO_API_CONTACT_ATTRIBUTES` (previous default: `{"VISIO_USER": True}`)
- `BREVO_API_TIMEOUT`
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost. They are replaced by a single `LASUITE_MARKETING` setting, configured through:
To migrate a local environment: | Variable | Default | Description |
| ------------------------------ | ------------------------------------------------ | -------------------------------------------- |
| `LASUITE_MARKETING_BACKEND` | `lasuite.marketing.backends.dummy.DummyBackend` | Backend class path |
| `LASUITE_MARKETING_PARAMETERS` | `{}` | Keyword arguments passed to the backend |
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans` ⚠️ The default backend is now a **dummy** (no-op). If you previously used
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media` Brevo, you must explicitly configure it, otherwise signups are silently dropped:
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
### Summary service and metadata collector: boto3 replaces the minio client LASUITE_MARKETING_BACKEND=lasuite.marketing.backends.brevo.BrevoBackend
LASUITE_MARKETING_PARAMETERS={"api_key": "<your-brevo-api-key>", "api_contact_list_ids": [1, 2], "api_contact_attributes": {"VISIO_USER": True}}
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings. Migration mapping:
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`, then to `us-east-1`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
Also: - `BREVO_API_KEY` → `api_key`
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs - `BREVO_API_CONTACT_LIST_IDS` → `api_contact_list_ids`
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`. - `BREVO_API_CONTACT_ATTRIBUTES` → `api_contact_attributes` (re-add
`{"VISIO_USER": True}` if you relied on the old default)
- `BREVO_API_TIMEOUT` → no equivalent (the request runs in a background task)
### Helm chart: media services default to Garage Note: `BREVO_API_KEY` used to support being read from a secret file; the API key
now lives inside `LASUITE_MARKETING_PARAMETERS`, so adapt how you inject that
secret (e.g. build the whole variable from your secret store).
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
### Recording encoding settings replaced by a resolution/profile model ### Recording encoding settings replaced by a resolution/profile model
The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder
@@ -175,6 +186,46 @@ Before enabling it:
See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding) See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding)
for the full setting reference, the shipped profile table and the tuning caveats. for the full setting reference, the shipped profile table and the tuning caveats.
## v1.33.0
### Purging inactive rooms
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md).
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`.
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading.
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window.
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged.
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL.
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
### Local development: MinIO replaced by Garage
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost.
To migrate a local environment:
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans`
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media`
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
### Summary service and metadata collector: boto3 replaces the minio client
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings.
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
Also:
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`.
### Helm chart: media services default to Garage
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
## v1.30.0 ## v1.30.0
### Removing S3 storage-event webhooks for recordings ### Removing S3 storage-event webhooks for recordings
+9 -8
View File
@@ -104,15 +104,16 @@ k8s_yaml(secret_yaml_generic(
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .')) k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .'))
k8s_resource('garage-cors', resource_deps=['garage']) k8s_resource('dev-backend-garage-cors', resource_deps=['dev-backend-garage'])
k8s_resource('meet-backend', resource_deps=['postgresql', 'garage-cors', 'redis', 'livekit-livekit-server']) k8s_resource('dev-backend-keycloak', resource_deps=['dev-backend-keycloak-pg'])
k8s_resource('meet-celery-backend', resource_deps=['redis']) k8s_resource('meet-backend', resource_deps=['dev-backend-postgres', 'dev-backend-garage-cors', 'dev-backend-redis', 'dev-backend-keycloak', 'livekit-livekit-server'])
k8s_resource('meet-celery-summarize', resource_deps=['redis']) k8s_resource('meet-celery-backend', resource_deps=['dev-backend-redis'])
k8s_resource('meet-celery-summary-backend', resource_deps=['redis']) k8s_resource('meet-celery-summarize', resource_deps=['dev-backend-redis'])
k8s_resource('meet-celery-transcribe-default', resource_deps=['redis']) k8s_resource('meet-celery-summary-backend', resource_deps=['dev-backend-redis'])
k8s_resource('livekit-livekit-server', resource_deps=['redis']) k8s_resource('meet-celery-transcribe-default', resource_deps=['dev-backend-redis'])
k8s_resource('livekit-livekit-server', resource_deps=['dev-backend-redis'])
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server']) k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
k8s_resource('keycloak', resource_deps=['kc-postgresql']) k8s_resource('livekit-egress', resource_deps=['livekit-livekit-server'])
# Trigger once on launch # Trigger once on launch
k8s_resource( k8s_resource(
'meet-backend-createsuperuser', 'meet-backend-createsuperuser',
+5 -8
View File
@@ -9,6 +9,8 @@ services:
redis: redis:
image: redis:5 image: redis:5
ports:
- "6379:6379"
mailcatcher: mailcatcher:
image: sj26/mailcatcher:latest image: sj26/mailcatcher:latest
@@ -271,11 +273,6 @@ services:
- ./src/agents:/app - ./src/agents:/app
- /app/.venv - /app/.venv
redis-summary:
image: redis:5
ports:
- "6379:6379"
app-summary-dev: app-summary-dev:
build: build:
context: src/summary context: src/summary
@@ -290,7 +287,7 @@ services:
volumes: volumes:
- ./src/summary:/app - ./src/summary:/app
depends_on: depends_on:
- redis-summary - redis
celery-summary-transcribe: celery-summary-transcribe:
container_name: celery-summary-transcribe container_name: celery-summary-transcribe
@@ -304,7 +301,7 @@ services:
volumes: volumes:
- ./src/summary:/app - ./src/summary:/app
depends_on: depends_on:
- redis-summary - redis
- app-summary-dev - app-summary-dev
- garage - garage
develop: develop:
@@ -324,7 +321,7 @@ services:
volumes: volumes:
- ./src/summary:/app - ./src/summary:/app
depends_on: depends_on:
- redis-summary - redis
- app-summary-dev - app-summary-dev
- garage - garage
develop: develop:
+31
View File
@@ -107,6 +107,37 @@ $ npm i
$ npm run dev $ npm run dev
``` ```
### LiveKit agents (optional)
The LiveKit agents are not started by `make run`. Each one runs its own
container and stays connected to LiveKit, which costs CPU and memory you
don't need unless you work on the features they power. Start them only
when you need them.
| Agent | Feature | Make command | Setting in `env.d/development/common` |
|---|---|---|---|
| `metadata-collector-dev` | Recording metadata (used to identify speakers in transcripts) | `make run-agent-metadata-collector` | `METADATA_COLLECTOR_ENABLED=True` |
| `multi-user-transcriber-dev` | Live subtitles | `make run-agent-multi-user-transcriber` | `ROOM_SUBTITLE_ENABLED=True` |
To start both at once:
```shellscript
$ make run-agents
```
Then set the matching settings to `True` and restart the backend so it
picks them up:
```shellscript
$ make run-backend
```
The multi-user transcriber also needs a speech-to-text provider. Configure
`STT_PROVIDER` and its credentials in
`env.d/development/multi_user_transcriber`.
Keep the settings and the agents in sync: if a setting is `True` while its
agent is stopped, the backend still dispatches jobs to it and the feature
fails silently.
--- ---
## Adding Content ## Adding Content
+4 -3
View File
@@ -104,10 +104,11 @@ ROOM_TELEPHONY_ENABLED=True
# ROOMKIT_ENABLED = True # ROOMKIT_ENABLED = True
# ROOMKIT_SERVER_TO_SERVER_API_TOKEN = ThisIsAnExampleKeyForDevPurposeOnly # ROOMKIT_SERVER_TO_SERVER_API_TOKEN = ThisIsAnExampleKeyForDevPurposeOnly
# Metadata # LiveKit agents (opt-in, start them with `make run-agents`)
METADATA_COLLECTOR_ENABLED=True # Metadata (requires the metadata-collector agent)
METADATA_COLLECTOR_ENABLED=False
# Subtitle # Subtitle (requires the multi-user-transcriber agent)
ROOM_SUBTITLE_ENABLED=False ROOM_SUBTITLE_ENABLED=False
FRONTEND_USE_FRENCH_GOV_FOOTER=False FRONTEND_USE_FRENCH_GOV_FOOTER=False
+4
View File
@@ -9,6 +9,10 @@ AWS_S3_ACCESS_KEY_ID="meet-access-key"
AWS_S3_SECRET_ACCESS_KEY="meet-secret-access-key" AWS_S3_SECRET_ACCESS_KEY="meet-secret-access-key"
AWS_S3_REGION_NAME="local" AWS_S3_REGION_NAME="local"
CELERY_BROKER_URL="redis://redis:6379/2"
CELERY_RESULT_BACKEND="redis://redis:6379/2"
TASK_TRACKER_REDIS_URL="redis://redis:6379/2"
WHISPERX_BASE_URL="https://configure-your-url.com" WHISPERX_BASE_URL="https://configure-your-url.com"
WHISPERX_ASR_MODEL="large-v2" WHISPERX_ASR_MODEL="large-v2"
WHISPERX_API_KEY="your-secret-key" WHISPERX_API_KEY="your-secret-key"
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "agents" name = "agents"
version = "1.33.0" version = "1.34.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"livekit-agents==1.7.0", "livekit-agents==1.7.0",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]] [[package]]
name = "agents" name = "agents"
version = "1.33.0" version = "1.34.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "boto3" }, { name = "boto3" },
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project] [project]
name = "meet" name = "meet"
version = "1.33.0" version = "1.34.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }] authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1297,7 +1297,7 @@ wheels = [
[[package]] [[package]]
name = "meet" name = "meet"
version = "1.33.0" version = "1.34.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
+280 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "meet", "name": "meet",
"version": "1.33.0", "version": "1.34.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "meet", "name": "meet",
"version": "1.33.0", "version": "1.34.0",
"dependencies": { "dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0", "@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
"@fontsource-variable/lexend": "5.3.0", "@fontsource-variable/lexend": "5.3.0",
@@ -64,7 +64,8 @@
"typescript-eslint": "8.60.1", "typescript-eslint": "8.60.1",
"vite": "8.0.14", "vite": "8.0.14",
"vite-plugin-static-copy": "4.1.1", "vite-plugin-static-copy": "4.1.1",
"vite-plugin-svgr": "5.2.0" "vite-plugin-svgr": "5.2.0",
"vitest": "5.0.2"
} }
}, },
"node_modules/@adobe/react-spectrum": { "node_modules/@adobe/react-spectrum": {
@@ -942,9 +943,9 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -2506,6 +2507,24 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/dom-mediacapture-record": { "node_modules/@types/dom-mediacapture-record": {
"version": "1.0.22", "version": "1.0.22",
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
@@ -2786,6 +2805,64 @@
} }
} }
}, },
"node_modules/@vitest/mocker": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz",
"integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.31",
"@vitest/spy": "5.0.2",
"estree-walker": "^3.0.3",
"magic-string": "^1.2.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/mocker/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/@vitest/mocker/node_modules/magic-string": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.6.0"
}
},
"node_modules/@vitest/spy": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz",
"integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.25", "version": "3.5.25",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
@@ -3151,6 +3228,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/ast-types-flow": { "node_modules/ast-types-flow": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -4130,6 +4217,16 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5030,6 +5127,13 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-module-lexer": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
"dev": true,
"license": "MIT"
},
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -5532,6 +5636,16 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/express": { "node_modules/express": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
@@ -8647,6 +8761,20 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/obug": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -10220,6 +10348,13 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/std-env": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
"integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
"dev": true,
"license": "MIT"
},
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -10526,6 +10661,26 @@
"xtend": "~4.0.1" "xtend": "~4.0.1"
} }
}, },
"node_modules/tinybench": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz",
"integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/tinyexec": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.17", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -11610,6 +11765,112 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/vitest": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz",
"integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/mocker": "5.0.2",
"chai": "^6.2.2",
"es-module-lexer": "^2.3.2",
"expect-type": "^1.4.0",
"magic-string": "^1.2.3",
"obug": "^2.1.4",
"picomatch": "^4.0.7",
"std-env": "^4.2.0",
"tinybench": "^6.1.4",
"tinyexec": "^1.3.0",
"tinyglobby": "^0.2.17",
"why-is-node-running": "^3.2.1"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "5.0.2",
"@vitest/browser-preview": "5.0.2",
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
"@vitest/coverage-istanbul": "5.0.2",
"@vitest/coverage-v8": "5.0.2",
"@vitest/ui": "5.0.2",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"optional": true
},
"@vitest/coverage-istanbul": {
"optional": true
},
"@vitest/coverage-v8": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
},
"vite": {
"optional": false
}
}
},
"node_modules/vitest/node_modules/magic-string": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.6.0"
}
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/walk-sync": { "node_modules/walk-sync": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
@@ -11794,6 +12055,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/why-is-node-running": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz",
"integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==",
"dev": true,
"license": "MIT",
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=20.11"
}
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+4 -2
View File
@@ -1,12 +1,13 @@
{ {
"name": "meet", "name": "meet",
"private": true, "private": true,
"version": "1.33.0", "version": "1.34.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "panda codegen && vite", "dev": "panda codegen && vite",
"build": "panda codegen && tsc -b && vite build", "build": "panda codegen && tsc -b && vite build",
"build:debug": "VITE_ANALYZE=true npm run build -- --debug", "build:debug": "VITE_ANALYZE=true npm run build -- --debug",
"test": "panda codegen && vitest run",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"preview": "vite preview", "preview": "vite preview",
@@ -71,6 +72,7 @@
"typescript-eslint": "8.60.1", "typescript-eslint": "8.60.1",
"vite": "8.0.14", "vite": "8.0.14",
"vite-plugin-static-copy": "4.1.1", "vite-plugin-static-copy": "4.1.1",
"vite-plugin-svgr": "5.2.0" "vite-plugin-svgr": "5.2.0",
"vitest": "5.0.2"
} }
} }
@@ -7,7 +7,6 @@ import {
useSwipe, useSwipe,
} from '@livekit/components-react' } from '@livekit/components-react'
import { mergeProps } from '@/utils/mergeProps' import { mergeProps } from '@/utils/mergeProps'
import { PaginationIndicator } from './PaginationIndicator'
import { useGridLayout } from '../hooks/useGridLayout' import { useGridLayout } from '../hooks/useGridLayout'
import { PaginationControl } from './PaginationControl' import { PaginationControl } from './PaginationControl'
import { useSpeakerPromotionTrigger } from '../hooks/useSpeakerPromotionTrigger' import { useSpeakerPromotionTrigger } from '../hooks/useSpeakerPromotionTrigger'
@@ -62,13 +61,7 @@ export function GridLayout({ tracks, ...props }: GridLayoutProps) {
> >
<TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop> <TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
{tracks.length > layout.maxTiles && ( {tracks.length > layout.maxTiles && (
<> <PaginationControl {...pagination} focusShortcut />
<PaginationIndicator
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
/>
<PaginationControl pagesContainer={gridEl} {...pagination} />
</>
)} )}
</div> </div>
) )
@@ -1,99 +1,147 @@
import * as React from 'react'
import { createInteractingObservable } from '@livekit/components-core'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react' import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { Button } from '@/primitives' import { Button } from '@/primitives'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css' import { css, cva, type RecipeVariantProps } from '@/styled-system/css'
import { useCallback, useRef } from 'react'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
export interface PaginationControlProps { const paginationToolbar = cva({
base: {
display: 'flex',
alignItems: 'center',
gap: '0.125rem',
backgroundColor: 'primaryDark.50',
borderRadius: '2rem',
border: '1px solid',
borderColor: 'primaryDark.200',
padding: '0.375rem',
},
variants: {
placement: {
overlay: {
position: 'absolute',
bottom: '1rem',
left: '50%',
zIndex: 2,
transform: 'translateX(-50%)',
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.45)',
},
inline: {
alignSelf: 'center',
flexShrink: 0,
marginTop: '1rem',
},
},
},
defaultVariants: {
placement: 'overlay',
},
})
export type PaginationControlProps = RecipeVariantProps<
typeof paginationToolbar
> & {
totalPageCount: number totalPageCount: number
nextPage: () => void nextPage: () => void
prevPage: () => void prevPage: () => void
currentPage: number currentPage: number
pagesContainer?: React.RefObject<HTMLElement> // The shortcut listens on the main window only, so a single instance may own it.
focusShortcut?: boolean
} }
const arrowButtonClass = css({
_disabled: {
cursor: 'default',
backgroundColor: 'transparent !important',
'& svg': {
opacity: 0.35,
},
_focusVisible: {
outline: '2px solid',
outlineColor: 'focusRing',
outlineOffset: '2px',
},
},
})
export function PaginationControl({ export function PaginationControl({
totalPageCount, totalPageCount,
nextPage, nextPage,
prevPage, prevPage,
currentPage, currentPage,
pagesContainer: connectedElement, placement,
focusShortcut = false,
}: PaginationControlProps) { }: PaginationControlProps) {
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' }) const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
const [interactive, setInteractive] = useState(false) const prevButtonRef = useRef<HTMLButtonElement>(null)
const nextButtonRef = useRef<HTMLButtonElement>(null)
const isSinglePage = totalPageCount <= 1
const isFirstPage = currentPage <= 1
const isLastPage = currentPage >= totalPageCount
useEffect(() => { const focusPagination = useCallback(() => {
let subscription: const target = isLastPage ? prevButtonRef.current : nextButtonRef.current
| ReturnType<ReturnType<typeof createInteractingObservable>['subscribe']> target?.focus()
| undefined }, [isLastPage])
if (connectedElement) {
subscription = createInteractingObservable(
connectedElement.current,
2000
).subscribe(setInteractive)
}
return () => {
if (subscription) {
subscription.unsubscribe()
}
}
}, [connectedElement])
if (totalPageCount <= 1) return null useRegisterKeyboardShortcut({
id: focusShortcut ? 'focus-pagination' : undefined,
handler: focusPagination,
isDisabled: isSinglePage,
unregisterOnUnmount: true,
})
if (isSinglePage) return null
const pageCount = t('count', { currentPage, totalPageCount })
return ( return (
<nav <div
aria-label={t('label')} role="group"
className={css({ aria-label={`${t('label')}, ${pageCount}`}
position: 'absolute', className={paginationToolbar({ placement })}
bottom: '1rem',
left: '50%',
transform: 'translateX(-50%)',
alignItems: 'stretch',
backgroundColor: 'var(--lk-control-bg)',
borderRadius: 'var(--lk-border-radius)',
transition: 'opacity ease-in-out .15s',
display: 'none',
border: '1px solid',
borderColor: 'primaryDark.100',
overflow: 'hidden',
})}
style={{
display: interactive ? 'flex' : 'none',
}}
data-lk-user-interaction={interactive}
> >
<Button <Button
isDisabled={currentPage == 1} ref={prevButtonRef}
onPress={prevPage} aria-disabled={isFirstPage}
size="xs" onPress={isFirstPage ? undefined : prevPage}
variant="quaternaryText" size="sm"
square
variant="primaryTextDark"
className={arrowButtonClass}
aria-label={t('previous')} aria-label={t('previous')}
tooltip={t('previous')}
> >
<RiArrowLeftSLine /> <RiArrowLeftSLine size={20} />
</Button> </Button>
<span <span
role="status" role="status"
className={css({ className={css({
padding: '0.25rem 0.5rem', color: 'white',
fontSize: '0.8125rem',
fontWeight: 500,
minWidth: '3.5rem',
textAlign: 'center',
userSelect: 'none',
padding: '0 0.35rem',
whiteSpace: 'nowrap',
})} })}
> >
{t('count', { {pageCount}
currentPage,
totalPageCount,
})}
</span> </span>
<Button <Button
isDisabled={currentPage == totalPageCount} ref={nextButtonRef}
onPress={nextPage} aria-disabled={isLastPage}
size="xs" onPress={isLastPage ? undefined : nextPage}
variant="quaternaryText" size="sm"
square
variant="primaryTextDark"
className={arrowButtonClass}
aria-label={t('next')} aria-label={t('next')}
tooltip={t('next')}
> >
<RiArrowRightSLine /> <RiArrowRightSLine size={20} />
</Button> </Button>
</nav> </div>
) )
} }
@@ -1,30 +0,0 @@
import * as React from 'react'
export interface PaginationIndicatorProps {
totalPageCount: number
currentPage: number
}
export const PaginationIndicator: (
props: PaginationIndicatorProps & React.RefAttributes<HTMLDivElement>
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
HTMLDivElement,
PaginationIndicatorProps
>(function PaginationIndicator(
{ totalPageCount, currentPage }: PaginationIndicatorProps,
ref
) {
const bubbles = new Array(totalPageCount).fill('').map((_, index) => {
if (index + 1 === currentPage) {
return <span data-lk-active key={index} />
} else {
return <span key={index} />
}
})
return (
<div ref={ref} className="lk-pagination-indicator" aria-hidden="true">
{bubbles}
</div>
)
})
@@ -1,96 +0,0 @@
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { styled } from '@/styled-system/jsx'
interface PipPaginationProps {
totalPageCount: number
currentPage: number
nextPage: () => void
prevPage: () => void
}
export const PipPagination = ({
totalPageCount,
currentPage,
nextPage,
prevPage,
}: PipPaginationProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
if (totalPageCount <= 1) return null
return (
<Nav aria-label={t('label')}>
<ArrowButton
type="button"
onClick={prevPage}
disabled={currentPage === 1}
aria-label={t('previous')}
>
<RiArrowLeftSLine size={18} />
</ArrowButton>
<Counter role="status">
{t('count', { currentPage, totalPageCount })}
</Counter>
<ArrowButton
type="button"
onClick={nextPage}
disabled={currentPage === totalPageCount}
aria-label={t('next')}
>
<RiArrowRightSLine size={18} />
</ArrowButton>
</Nav>
)
}
const Nav = styled('nav', {
base: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
marginTop: '1rem',
flexShrink: 0,
},
})
const ArrowButton = styled('button', {
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '1.75rem',
height: '1.75rem',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
color: 'white',
backgroundColor: 'primaryDark.100',
transition: 'opacity 0.15s, background-color 0.15s',
'&:hover:not(:disabled)': {
backgroundColor: 'primaryDark.75',
},
'&:focus-visible': {
outline: '2px solid',
outlineColor: 'white',
outlineOffset: '2px',
},
'&:disabled': {
opacity: 0.3,
cursor: 'default',
},
},
})
const Counter = styled('span', {
base: {
fontSize: '0.75rem',
color: 'white',
opacity: 0.8,
whiteSpace: 'nowrap',
padding: '0 0.25rem',
minWidth: '3rem',
textAlign: 'center',
},
})
@@ -4,7 +4,7 @@ import { RoomEvent, Track } from 'livekit-client'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { PipFocusLayout } from './PipFocusLayout' import { PipFocusLayout } from './PipFocusLayout'
import { PipGridLayout } from './PipGridLayout' import { PipGridLayout } from './PipGridLayout'
import { PipPagination } from './PipPagination' import { PaginationControl } from '@/features/layout/components/PaginationControl'
import { PipScreenShareLayout } from './PipScreenShareLayout' import { PipScreenShareLayout } from './PipScreenShareLayout'
import { StageFrame } from './StageFrame' import { StageFrame } from './StageFrame'
import { MAX_PIP_TILES } from '../../utils/pipGrid' import { MAX_PIP_TILES } from '../../utils/pipGrid'
@@ -120,12 +120,7 @@ const PaginatedStage = ({
}) => ( }) => (
<StageWrapper> <StageWrapper>
<StageFrame>{children}</StageFrame> <StageFrame>{children}</StageFrame>
<PipPagination <PaginationControl {...pagination} placement="inline" />
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
nextPage={pagination.nextPage}
prevPage={pagination.prevPage}
/>
</StageWrapper> </StageWrapper>
) )
@@ -31,35 +31,20 @@ export const useSidePanel = () => {
const isSidePanelOpen = !!activePanelId const isSidePanelOpen = !!activePanelId
const isSubPanelOpen = !!activeSubPanelId const isSubPanelOpen = !!activeSubPanelId
const toggleAdmin = () => { // Reads the live store, not the render snapshot: shortcut handlers outlive
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN // toggles that unmount (e.g. inside a closed overflow menu).
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null const togglePanel = (panelId: PanelId) => {
layoutStore.activePanelId =
layoutStore.activePanelId === panelId ? null : panelId
if (layoutStore.activeSubPanelId) layoutStore.activeSubPanelId = null
} }
const toggleParticipants = () => { const toggleAdmin = () => togglePanel(PanelId.ADMIN)
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS const toggleParticipants = () => togglePanel(PanelId.PARTICIPANTS)
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null const toggleChat = () => togglePanel(PanelId.CHAT)
} const toggleEffects = () => togglePanel(PanelId.EFFECTS)
const toggleTools = () => togglePanel(PanelId.TOOLS)
const toggleChat = () => { const toggleInfo = () => togglePanel(PanelId.INFO)
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleEffects = () => {
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleTools = () => {
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleInfo = () => {
layoutStore.activePanelId = isInfoOpen ? null : PanelId.INFO
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const openTranscript = () => { const openTranscript = () => {
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest'
import { normalizeRoomId } from './isRoomValid'
describe('normalizeRoomId', () => {
it('lowercases and re-inserts the hyphens of a ten-letter id', () => {
expect(normalizeRoomId('ABCDEFGHIJ')).toBe('abc-defg-hij')
expect(normalizeRoomId('abc-defghij')).toBe('abc-defg-hij')
})
it('returns any other input unchanged', () => {
expect(normalizeRoomId('abc-def')).toBe('abc-def')
expect(normalizeRoomId('Not-A-Room')).toBe('Not-A-Room')
})
})
@@ -7,6 +7,7 @@ export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
export type ShortcutId = export type ShortcutId =
| 'open-shortcuts' | 'open-shortcuts'
| 'focus-toolbar' | 'focus-toolbar'
| 'focus-pagination'
| 'toggle-microphone' | 'toggle-microphone'
| 'toggle-camera' | 'toggle-camera'
| 'push-to-talk' | 'push-to-talk'
@@ -44,6 +45,11 @@ export const shortcutCatalog: ShortcutDescriptor[] = [
category: 'navigation', category: 'navigation',
shortcut: { key: 'F2' }, shortcut: { key: 'F2' },
}, },
{
id: 'focus-pagination',
category: 'navigation',
shortcut: { key: 'G', ctrlKey: true, shiftKey: true },
},
{ {
id: 'toggle-microphone', id: 'toggle-microphone',
category: 'media', category: 'media',
@@ -7,12 +7,16 @@ export type useRegisterKeyboardShortcutProps = {
id?: ShortcutId id?: ShortcutId
handler: () => Promise<void | boolean | undefined> | void handler: () => Promise<void | boolean | undefined> | void
isDisabled?: boolean isDisabled?: boolean
// Opt-in: controls rendered inside menus unmount when the menu closes, and
// their shortcuts must keep working.
unregisterOnUnmount?: boolean
} }
export const useRegisterKeyboardShortcut = ({ export const useRegisterKeyboardShortcut = ({
id, id,
handler, handler,
isDisabled = false, isDisabled = false,
unregisterOnUnmount = false,
}: useRegisterKeyboardShortcutProps) => { }: useRegisterKeyboardShortcutProps) => {
useEffect(() => { useEffect(() => {
if (!id) return if (!id) return
@@ -21,8 +25,14 @@ export const useRegisterKeyboardShortcut = ({
const formattedKey = formatShortcutKey(descriptor.shortcut) const formattedKey = formatShortcutKey(descriptor.shortcut)
if (isDisabled) { if (isDisabled) {
keyboardShortcutsStore.shortcuts.delete(formattedKey) keyboardShortcutsStore.shortcuts.delete(formattedKey)
} else { return
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
} }
}, [handler, id, isDisabled]) keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
if (!unregisterOnUnmount) return
return () => {
if (keyboardShortcutsStore.shortcuts.get(formattedKey) === handler) {
keyboardShortcutsStore.shortcuts.delete(formattedKey)
}
}
}, [handler, id, isDisabled, unregisterOnUnmount])
} }
+1
View File
@@ -784,6 +784,7 @@
"actions": { "actions": {
"open-shortcuts": "Tastenkürzel-Hilfe öffnen", "open-shortcuts": "Tastenkürzel-Hilfe öffnen",
"focus-toolbar": "Fokus auf die untere Symbolleiste", "focus-toolbar": "Fokus auf die untere Symbolleiste",
"focus-pagination": "Fokus auf die Teilnehmerseiten",
"toggle-microphone": "Mikrofon umschalten", "toggle-microphone": "Mikrofon umschalten",
"toggle-camera": "Kamera umschalten", "toggle-camera": "Kamera umschalten",
"push-to-talk": "Push-to-talk", "push-to-talk": "Push-to-talk",
+1
View File
@@ -784,6 +784,7 @@
"actions": { "actions": {
"open-shortcuts": "Open shortcuts help", "open-shortcuts": "Open shortcuts help",
"focus-toolbar": "Focus bottom toolbar", "focus-toolbar": "Focus bottom toolbar",
"focus-pagination": "Focus participant pagination",
"toggle-microphone": "Toggle microphone", "toggle-microphone": "Toggle microphone",
"toggle-camera": "Toggle camera", "toggle-camera": "Toggle camera",
"push-to-talk": "Push-to-talk (hold to unmute)", "push-to-talk": "Push-to-talk (hold to unmute)",
+1
View File
@@ -783,6 +783,7 @@
"actions": { "actions": {
"open-shortcuts": "Abrir la ayuda de atajos", "open-shortcuts": "Abrir la ayuda de atajos",
"focus-toolbar": "Poner el foco en la barra de herramientas inferior", "focus-toolbar": "Poner el foco en la barra de herramientas inferior",
"focus-pagination": "Poner el foco en la paginación de participantes",
"toggle-microphone": "Activar o desactivar el micrófono", "toggle-microphone": "Activar o desactivar el micrófono",
"toggle-camera": "Activar o desactivar la cámara", "toggle-camera": "Activar o desactivar la cámara",
"push-to-talk": "Pulsar para hablar (mantener para reactivar)", "push-to-talk": "Pulsar para hablar (mantener para reactivar)",
+1
View File
@@ -784,6 +784,7 @@
"actions": { "actions": {
"open-shortcuts": "Ouvrir l’aide des raccourcis", "open-shortcuts": "Ouvrir l’aide des raccourcis",
"focus-toolbar": "Mettre le focus sur la barre d’outils du bas", "focus-toolbar": "Mettre le focus sur la barre d’outils du bas",
"focus-pagination": "Mettre le focus sur la pagination des participants",
"toggle-microphone": "Activer ou désactiver le micro", "toggle-microphone": "Activer ou désactiver le micro",
"toggle-camera": "Activer ou désactiver la caméra", "toggle-camera": "Activer ou désactiver la caméra",
"push-to-talk": "Appuyer pour parler (maintenir pour réactiver)", "push-to-talk": "Appuyer pour parler (maintenir pour réactiver)",
+1
View File
@@ -784,6 +784,7 @@
"actions": { "actions": {
"open-shortcuts": "Sneltoetsenhulp openen", "open-shortcuts": "Sneltoetsenhulp openen",
"focus-toolbar": "Focus op de onderste werkbalk", "focus-toolbar": "Focus op de onderste werkbalk",
"focus-pagination": "Focus op de paginering van deelnemers",
"toggle-microphone": "Microfoon aan/uit", "toggle-microphone": "Microfoon aan/uit",
"toggle-camera": "Camera aan/uit", "toggle-camera": "Camera aan/uit",
"push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)", "push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)",
+1 -1
View File
@@ -10,5 +10,5 @@
"noEmit": true, "noEmit": true,
"types": ["node"] "types": ["node"]
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts", "vitest.config.ts"]
} }
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
// Kept apart from vite.config.ts so the tests load none of the build plugins.
export default defineConfig({
resolve: {
tsconfigPaths: true,
},
})
+25 -16
View File
@@ -24,7 +24,7 @@ _summaryEnvVars: &summaryEnvVars
APP_NAME: summary-microservice APP_NAME: summary-microservice
APP_API_TOKEN: password APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: http://garage.meet.svc.cluster.local:9000/ AWS_S3_ENDPOINT_URL: http://dev-backend-garage.meet.svc.cluster.local:9000/
AWS_S3_ACCESS_KEY_ID: meet-access-key AWS_S3_ACCESS_KEY_ID: meet-access-key
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
AWS_S3_SECURE_ACCESS: False AWS_S3_SECURE_ACCESS: False
@@ -69,9 +69,9 @@ _summaryEnvVars: &summaryEnvVars
LLM_MODEL: Qwen/Qwen3-Coder-30B-A3B-Instruct LLM_MODEL: Qwen/Qwen3-Coder-30B-A3B-Instruct
WEBHOOK_API_TOKEN: password WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/ WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1 CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1 CELERY_RESULT_BACKEND: redis://user:pass@dev-backend-redis:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1 TASK_TRACKER_REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED: true IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED: true
RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP: 0.5 RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP: 0.5
RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS: true RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS: true
@@ -127,12 +127,21 @@ backend:
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
# Databases # Databases
DB_HOST: postgres DB_HOST: dev-backend-postgres
DB_NAME: meet DB_NAME:
DB_USER: dinum secretKeyRef:
DB_PASSWORD: pass name: dev-backend-postgres
key: database
DB_USER:
secretKeyRef:
name: dev-backend-postgres
key: username
DB_PASSWORD:
secretKeyRef:
name: dev-backend-postgres
key: password
DB_PORT: 5432 DB_PORT: 5432
REDIS_URL: redis://default:pass@redis-master:6379/1 REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
# Static files # Static files
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
# Permissions # Permissions
@@ -163,7 +172,7 @@ backend:
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr" FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"
FRONTEND_IS_SILENT_LOGIN_ENABLED: False FRONTEND_IS_SILENT_LOGIN_ENABLED: False
# S3 Storage # S3 Storage
AWS_S3_ENDPOINT_URL: http://garage.meet.svc.cluster.local:9000 AWS_S3_ENDPOINT_URL: http://dev-backend-garage.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet-access-key AWS_S3_ACCESS_KEY_ID: meet-access-key
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
AWS_STORAGE_BUCKET_NAME: meet-media-storage AWS_STORAGE_BUCKET_NAME: meet-media-storage
@@ -186,7 +195,7 @@ backend:
MEDIA_BASE_URL: https://meet.127.0.0.1.nip.io MEDIA_BASE_URL: https://meet.127.0.0.1.nip.io
FILE_UPLOAD_ENABLED: True FILE_UPLOAD_ENABLED: True
CELERY_ENABLED: True CELERY_ENABLED: True
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1 CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
# Recording & Transcription # Recording & Transcription
RECORDING_ENABLE: True RECORDING_ENABLE: True
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/ SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/
@@ -266,11 +275,11 @@ ingressMedia:
annotations: annotations:
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/ nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256" nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: garage.meet.svc.cluster.local:9000 nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-garage.meet.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1 nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
serviceMedia: serviceMedia:
host: garage.meet.svc.cluster.local host: dev-backend-garage.meet.svc.cluster.local
port: 9000 port: 9000
# ---- Extra ingress/service for background file uploads ------------ # ---- Extra ingress/service for background file uploads ------------
@@ -282,11 +291,11 @@ ingressMediaFiles:
annotations: annotations:
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/files/media-auth/ nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/files/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256" nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: garage.meet.svc.cluster.local:9000 nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-garage.meet.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/files/$1 nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/files/$1
serviceMediaFiles: serviceMediaFiles:
host: garage.meet.svc.cluster.local host: dev-backend-garage.meet.svc.cluster.local
port: 9000 port: 9000
# ---- STT Orchestration Microservice Components -------------------- # ---- STT Orchestration Microservice Components --------------------
@@ -364,7 +373,7 @@ agentMetadata:
{{- end }} {{- end }}
{{- end }} {{- end }}
ENABLE_SILERO_VAD: "false" ENABLE_SILERO_VAD: "false"
AWS_S3_ENDPOINT_URL: garage.meet.svc.cluster.local:9000 AWS_S3_ENDPOINT_URL: dev-backend-garage.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet-access-key AWS_S3_ACCESS_KEY_ID: meet-access-key
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
AWS_S3_SECURE_ACCESS: False AWS_S3_SECURE_ACCESS: False
@@ -13,14 +13,15 @@ egress:
{{- end }} {{- end }}
{{- end }} {{- end }}
redis: redis:
address: redis-master:6379 address: dev-backend-redis:6379
username: user
password: pass password: pass
s3: s3:
access_key: meet-access-key access_key: meet-access-key
secret: meet-secret-access-key secret: meet-secret-access-key
region: local region: local
bucket: meet-media-storage bucket: meet-media-storage
endpoint: http://garage:9000 endpoint: http://dev-backend-garage:9000
force_path_style: true force_path_style: true
loadBalancer: loadBalancer:
@@ -14,7 +14,8 @@ livekit:
port_range_end: 60000 port_range_end: 60000
tcp_port: 7881 tcp_port: 7881
redis: redis:
address: redis-master:6379 address: dev-backend-redis:6379
username: user
password: pass password: pass
keys: keys:
turn: turn:
@@ -16,14 +16,15 @@ egress:
{{- end }} {{- end }}
{{- end }} {{- end }}
redis: redis:
address: redis-master:6379 address: dev-backend-redis:6379
username: user
password: pass password: pass
s3: s3:
access_key: meet-access-key access_key: meet-access-key
secret: meet-secret-access-key secret: meet-secret-access-key
region: local region: local
bucket: meet-media-storage bucket: meet-media-storage
endpoint: http://garage:9000 endpoint: http://dev-backend-garage:9000
force_path_style: true force_path_style: true
loadBalancer: loadBalancer:
@@ -14,7 +14,8 @@ livekit:
port_range_end: 60000 port_range_end: 60000
tcp_port: 7881 tcp_port: 7881
redis: redis:
address: redis-master:6379 address: dev-backend-redis:6379
username: user
password: pass password: pass
keys: keys:
turn: turn:
-172
View File
@@ -1,172 +0,0 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: garage
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 10m
spec:
rules:
- host: "garage.127.0.0.1.nip.io"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: garage
port:
number: 9000
tls:
- hosts:
- garage.127.0.0.1.nip.io
secretName: meet-tls
---
apiVersion: v1
kind: Service
metadata:
name: garage
namespace: {{ .Release.Namespace | quote }}
spec:
ports:
- name: client
port: 9000
protocol: TCP
targetPort: 9000
selector:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: garage
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: garage-config
namespace: {{ .Release.Namespace | quote }}
data:
garage.toml: |
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
replication_factor = 1
rpc_bind_addr = "127.0.0.1:3901"
rpc_public_addr = "127.0.0.1:3901"
[s3_api]
api_bind_addr = "[::]:9000"
# Clients must sign their requests for this region (AWS_S3_REGION_NAME)
s3_region = "local"
---
apiVersion: v1
kind: Secret
metadata:
name: garage-dev
namespace: {{ .Release.Namespace | quote }}
type: Opaque
data:
GARAGE_RPC_SECRET: {{ printf "%s/garage-rpc-secret" .Release.Namespace | sha256sum | b64enc }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: garage
namespace: {{ .Release.Namespace | quote }}
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: garage
spec:
selector:
matchLabels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: garage
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: garage
spec:
containers:
- name: garage
command:
- /garage
- server
- --single-node
- --default-bucket
env:
- name: GARAGE_RPC_SECRET
valueFrom:
secretKeyRef:
name: garage-dev
key: GARAGE_RPC_SECRET
- name: GARAGE_DEFAULT_ACCESS_KEY
value: meet-access-key
- name: GARAGE_DEFAULT_SECRET_KEY
value: meet-secret-access-key
- name: GARAGE_DEFAULT_BUCKET
value: meet-media-storage
image: "dxflrs/garage:v2.4.1"
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9000
name: client
readinessProbe:
exec:
command:
- /garage
- health
volumeMounts:
- mountPath: /etc/garage.toml
name: config
subPath: garage.toml
- mountPath: /var/lib/garage
name: data
volumes:
- name: config
configMap:
name: garage-config
- name: data
emptyDir:
---
# Garage denies cross-origin requests by default: allow the frontend to upload
# files straight to the bucket
apiVersion: batch/v1
kind: Job
metadata:
name: garage-cors
spec:
template:
spec:
containers:
- name: aws-cli
image: amazon/aws-cli:2.37.1
env:
- name: AWS_ACCESS_KEY_ID
value: meet-access-key
- name: AWS_SECRET_ACCESS_KEY
value: meet-secret-access-key
- name: AWS_DEFAULT_REGION
value: local
- name: AWS_ENDPOINT_URL
value: http://garage:9000
- name: BUCKET
value: meet-media-storage
command:
- /bin/sh
- -c
- |
deadline=$(($(date +%s) + 300))
until aws s3api head-bucket --bucket="$BUCKET" --cli-connect-timeout=5; do
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "Bucket $BUCKET still unavailable on $AWS_ENDPOINT_URL" >&2
exit 1
fi
echo "Waiting for bucket $BUCKET on $AWS_ENDPOINT_URL"
sleep 5
done
exec aws s3api put-bucket-cors --bucket="$BUCKET" \
--cors-configuration='{"CORSRules": [{"AllowedOrigins": ["https://meet.127.0.0.1.nip.io"], "AllowedMethods": ["GET", "HEAD", "PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"]}]}'
restartPolicy: Never
backoffLimit: 3
@@ -1,61 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: kc-postgres
namespace: {{ .Release.Namespace | quote }}
spec:
ports:
- name: tcp-postgresql
port: 5432
protocol: TCP
targetPort: tcp-postgresql
selector:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: kc-postgresql
type: ClusterIP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kc-postgresql
namespace: {{ .Release.Namespace | quote }}
spec:
selector:
matchLabels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: kc-postgresql
serviceName: "kc-postgres"
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: kc-postgresql
spec:
terminationGracePeriodSeconds: 10
containers:
- name: pg
image: postgres:16-alpine
ports:
- containerPort: 5432
name: tcp-postgresql
env:
- name: POSTGRES_PASSWORD
value: pass
- name: POSTGRES_USER
value: dinum
- name: POSTGRES_DB
value: keycloak
volumeMounts:
- name: data
mountPath: /var/lib/postgresql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
-104
View File
@@ -1,104 +0,0 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
spec:
rules:
- host: "keycloak.127.0.0.1.nip.io"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
tls:
- hosts:
- keycloak.127.0.0.1.nip.io
secretName: meet-tls
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: {{ .Release.Namespace | quote }}
spec:
ports:
- name: tcp-keycloak
port: 8080
protocol: TCP
targetPort: tcp-keycloak
selector:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: keycloak
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: realm
data:
meet.json: |
{{ .Values.realm | indent 4 }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: keycloak
namespace: {{ .Release.Namespace | quote }}
spec:
selector:
matchLabels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: keycloak
serviceName: "keycloak"
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: keycloak
spec:
terminationGracePeriodSeconds: 10
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:20.0.1
args:
- start-dev
- --features=preview
- --import-realm
- --proxy=edge
- --hostname=keycloak.127.0.0.1.nip.io
- --hostname-strict=false
- --hostname-strict-https=false
ports:
- containerPort: 8080
name: tcp-keycloak
env:
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
value: admin
- name: PROXY_ADDRESS_FORWARDING
value: 'true'
- name: KC_DB_URL_HOST
value: kc_postgresql
- name: KC_DB_URL_DATABASE
value: keycloak
- name: KC_DB_PASSWORD
value: pass
- name: KC_DB_USERNAME
value: dinum
- name: KC_DB_SCHEMA
value: public
volumeMounts:
- name: realm
mountPath: "/opt/keycloak/data/import"
readOnly: true
volumes:
- name: realm
configMap:
name: realm
@@ -1,70 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: {{ .Release.Namespace | quote }}
spec:
ports:
- name: tcp-postgresql
port: 5432
protocol: TCP
targetPort: tcp-postgresql
selector:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: postgresql
type: ClusterIP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: {{ .Release.Namespace | quote }}
spec:
selector:
matchLabels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: postgresql
serviceName: "postgres"
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: postgresql
spec:
terminationGracePeriodSeconds: 10
containers:
- name: pg
image: postgres:16-alpine
readinessProbe:
exec:
command: [ "pg_isready", "-U", "dinum", "-d", "meet", "-h", "127.0.0.1" ]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: [ "pg_isready", "-U", "dinum", "-d", "meet", "-h", "127.0.0.1" ]
initialDelaySeconds: 15
periodSeconds: 10
ports:
- containerPort: 5432
name: tcp-postgresql
env:
- name: POSTGRES_PASSWORD
value: pass
- name: POSTGRES_USER
value: dinum
- name: POSTGRES_DB
value: meet
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
-65
View File
@@ -1,65 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: redis-master
namespace: {{ .Release.Namespace | quote }}
spec:
ports:
- name: tcp-redis
port: 6379
protocol: TCP
targetPort: tcp-redis
selector:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: redis
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis
data:
redis.conf: |
bind 0.0.0.0
port 6379
user default on >pass ~* &* +@all
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: {{ .Release.Namespace | quote }}
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: redis
spec:
selector:
matchLabels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: redis
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/instance: extra
app.kubernetes.io/name: redis
spec:
containers:
- name: redis
args:
- redis-server
- /usr/local/etc/redis/redis.conf
image: "redis:8.2-alpine"
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
name: tcp-redis
volumeMounts:
- name: redis
mountPath: "/usr/local/etc/redis"
readOnly: true
volumes:
- name: redis
configMap:
name: redis
+56 -2
View File
@@ -13,6 +13,9 @@ repositories:
- name: livekit - name: livekit
url: https://helm.livekit.io url: https://helm.livekit.io
- name: dev-backends
url: https://suitenumerique.github.io/helm-dev-backend
releases: releases:
- name: extra - name: extra
installed: {{ regexMatch "^dev.*" .Environment.Name }} installed: {{ regexMatch "^dev.*" .Environment.Name }}
@@ -27,8 +30,59 @@ releases:
- enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}} - enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
- oldDomain: {{ .Values | get "oldDomain" "demo.com" }} - oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
- newDomain: {{ .Values | get "newDomain" "demo.com" }} - newDomain: {{ .Values | get "newDomain" "demo.com" }}
- realm: |
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }} - name: dev-backend
installed: {{ regexMatch "^dev.*" .Environment.Name }}
namespace: {{ .Namespace }}
chart: dev-backends/dev-backend
version: 0.0.14
values:
- postgres:
enabled: true
username: dinum
password: pass
database: meet
size: 1Gi
- redis:
enabled: true
username: user
password: pass
- garage:
enabled: true
accessKey: meet-access-key
secretKey: meet-secret-access-key
bucket: meet-media-storage
region: local
persistence: false
ingress:
enabled: true
hostname: garage.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 10m
tls:
enabled: true
secretName: meet-tls
cors:
allowedOrigins:
- https://meet.127.0.0.1.nip.io
- keycloak:
enabled: true
hostname: keycloak.127.0.0.1.nip.io
username: admin
password: admin
tls:
enabled: true
secretName: meet-tls
db:
username: dinum
password: pass
database: keycloak
size: 1Gi
realm:
name: meet
username: meet
password: meet
email: meet@example.com
- name: meet - name: meet
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.33.0", "version": "1.34.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.33.0", "version": "1.34.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@html-to/text-cli": "0.6.1", "@html-to/text-cli": "0.6.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.33.0", "version": "1.34.0",
"description": "An util to generate html and text django's templates from mjml templates", "description": "An util to generate html and text django's templates from mjml templates",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "sdk", "name": "sdk",
"version": "1.33.0", "version": "1.34.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sdk", "name": "sdk",
"version": "1.33.0", "version": "1.34.0",
"license": "ISC", "license": "ISC",
"workspaces": [ "workspaces": [
"./library", "./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "sdk", "name": "sdk",
"version": "1.33.0", "version": "1.34.0",
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "summary" name = "summary"
version = "1.33.0" version = "1.34.0"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"fastapi[standard]>=0.105.0", "fastapi[standard]>=0.105.0",
+1 -1
View File
@@ -1484,7 +1484,7 @@ wheels = [
[[package]] [[package]]
name = "summary" name = "summary"
version = "1.33.0" version = "1.34.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "boto3" }, { name = "boto3" },