Compare commits

..

2 Commits

Author SHA1 Message Date
lebaudantoine 2c678087b3 🔖(addons) remove the beta tag
Promote the addon to a stable v1 release.
2026-07-21 10:25:27 +02:00
lebaudantoine b6a57b5545 (addon) show add-in tools when creating meetings in shared calendars
The existing manifest was not exposing the add-in tools when
creating a meeting in a shared calendar. Amend the manifest with the
missing instructions so the add-in shows up in that context as well.

Manifest changes generated with Claude's assistance.
2026-07-21 10:16:14 +02:00
30 changed files with 197 additions and 245 deletions
-16
View File
@@ -8,21 +8,6 @@ and this project adheres to
## [Unreleased]
### Changed
- ⬆️(frontend) upgrade @mediapipe/tasks-vision from 0.10.14 to 0.10.35
- ⬆️(frontend) upgrade i18next from 26.3.1 to 26.3.2
- ⬆️(frontend) upgrade posthog-js from 1.391.2 to 1.395.0
- ⬆️(frontend) upgrade @tanstack/react-query from 5.101.0 to 5.101.1
- ⬆️(frontend) upgrade livekit-client from 2.19.2 to 2.20.0
## Fixed
- 🐛(transcription) fix silent bug in speaker assignment
- 🐛(backend) fix bug linked to recording state inconsistency
## [1.24.0] - 2026-07-21
### Added
- ✨(backend) allow searching the recording admin table by owner email
@@ -49,7 +34,6 @@ and this project adheres to
- 🩹(backend) identify externally provisioned users to PostHog
- 🐛(backend) fix info panel crash for unregistered rooms
- ♿️(frontend) focus side panel container on open #1452
- 🐛(summary) whisper call error handling
## [1.23.0] - 2026-07-08
-1
View File
@@ -164,7 +164,6 @@ echo " - src/backend/pyproject.toml"
echo " - src/backend/uv.lock"
echo " - src/summary/pyproject.toml"
echo " - src/agents/pyproject.toml"
echo " - src/agents/uv.lock"
echo " - CHANGELOG.md"
echo ""
print_warning "Next steps:"
+4 -4
View File
@@ -11,14 +11,14 @@ There are two ways to customize LaSuite Meet:
### How to Use
To use this feature, simply set the `FRONTEND_CUSTOM_CSS_URL` environment variable (of the **backend** service) to the URL of your custom CSS file. For example:
To use this feature, simply set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. For example:
```javascript
FRONTEND_CUSTOM_CSS_URL=https://example.com/custom-style.css
FRONTEND_CSS_URL=https://example.com/custom-style.css
```
> [!TIP]
> If you serve your CSS file on the same domain as LaSuite Meet, paths are supported, i.e. `FRONTEND_CUSTOM_CSS_URL=/custom/style.css` will load `https://your-domain.com/custom/style.css`.
> If you serve your CSS file on the same domain as LaSuite Meet, paths are supported, i.e. `FRONTEND_CSS_URL=/custom/style.css` will load `https://your-domain.com/custom/style.css`.
Setting this variable makes the app load your CSS at runtime, adding a `<link>` to `<head>` so you can override CSS variables and customize the frontend without rebuilding.
@@ -37,7 +37,7 @@ Let's say you want to change the font of our application to a custom font. You c
}
```
Then, set the `FRONTEND_CUSTOM_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the default font to the one you specified.
Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the default font to the one you specified.
> [!IMPORTANT]
> You can override any CSS token—semantic or palette. See [panda.config.ts](../src/frontend/panda.config.ts) for all defined semantic tokens.
+4 -4
View File
@@ -10,7 +10,7 @@
"license": "MIT",
"dependencies": {
"core-js": "3.49.0",
"i18next": "^26.3.2",
"i18next": "26.3.1",
"i18next-browser-languagedetector": "8.2.1",
"regenerator-runtime": "0.14.1"
},
@@ -9364,9 +9364,9 @@
}
},
"node_modules/i18next": {
"version": "26.3.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.2.tgz",
"integrity": "sha512-QQkXAM1sPDHqhxMQuBeHVMUn6mJchF+wdpOoQerciLAFqO3ZYdxO0EUbeEhruyutnNwpUQIITDVzLjwnNL0T1w==",
"version": "26.3.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
"funding": [
{
"type": "individual",
+1 -1
View File
@@ -27,7 +27,7 @@
},
"dependencies": {
"core-js": "3.49.0",
"i18next": "26.3.2",
"i18next": "26.3.1",
"i18next-browser-languagedetector": "8.2.1",
"regenerator-runtime": "0.14.1"
},
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.24.0"
version = "1.23.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.6.4",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]]
name = "agents"
version = "1.24.0"
version = "1.23.0"
source = { virtual = "." }
dependencies = [
{ name = "livekit-agents" },
@@ -9,11 +9,6 @@ from livekit import api
from core import models, utils
from core.models import Recording
from core.recording.event.notification import notification_service
from core.services.room_management import (
RoomManagement,
RoomManagementException,
RoomNotFoundException,
)
logger = getLogger(__name__)
@@ -44,15 +39,10 @@ class RecordingEventsService:
recording_status = status_mapping.get(egress_status)
if recording_status:
try:
RoomManagement().update_metadata(
utils.update_room_metadata(
room_name, {"recording_status": recording_status}
)
except RoomNotFoundException:
logger.info(
"LiveKit room %s no longer exists, skipping metadata update",
room_name,
)
except RoomManagementException as e:
except utils.MetadataUpdateException as e:
logger.exception("Failed to update room's metadata: %s", e)
@staticmethod
+3 -12
View File
@@ -2,12 +2,8 @@
import logging
from core import utils
from core.models import Recording, RecordingStatusChoices
from core.services.room_management import (
RoomManagement,
RoomManagementException,
RoomNotFoundException,
)
from .exceptions import (
RecordingStartError,
@@ -68,15 +64,10 @@ class WorkerServiceMediator:
mode = recording.options.get("original_mode", None) or recording.mode
try:
RoomManagement().update_metadata(
utils.update_room_metadata(
room_name, {"recording_mode": mode, "recording_status": "starting"}
)
except RoomNotFoundException:
logger.info(
"LiveKit room %s no longer exists, skipping metadata update",
room_name,
)
except RoomManagementException as e:
except utils.MetadataUpdateException as e:
logger.exception("Failed to update room's metadata: %s", e)
logger.info(
+4 -14
View File
@@ -11,7 +11,7 @@ from django.conf import settings
from livekit import api
from core import models
from core import models, utils
from core.recording.services.metadata_collector import (
MetadataCollectorException,
MetadataCollectorService,
@@ -23,11 +23,6 @@ from core.recording.services.recording_events import (
)
from .lobby import LobbyService
from .room_management import (
RoomManagement,
RoomManagementException,
RoomNotFoundException,
)
from .telephony import TelephonyException, TelephonyService
logger = getLogger(__name__)
@@ -182,15 +177,10 @@ class LiveKitEventsService:
try:
room_name = str(recording.room.id)
RoomManagement().update_metadata(
room_name, remove_keys=["recording_mode", "recording_status"]
utils.update_room_metadata(
room_name, {}, ["recording_mode", "recording_status"]
)
except RoomNotFoundException:
logger.info(
"LiveKit room %s no longer exists, skipping metadata update",
room_name,
)
except RoomManagementException as e:
except utils.MetadataUpdateException as e:
logger.exception("Failed to update room's metadata: %s", e)
if recording.options.get("metadata_collector_dispatch_id", None) is not None:
+3 -29
View File
@@ -8,7 +8,6 @@ from typing import Dict, Optional
from asgiref.sync import async_to_sync
from livekit.api import (
ListRoomsRequest,
TwirpError,
UpdateRoomMetadataRequest,
)
@@ -30,45 +29,20 @@ class RoomManagement:
"""Service for managing LiveKit rooms."""
@async_to_sync
async def update_metadata(
self,
room_name: str,
metadata: Optional[Dict] = None,
remove_keys: Optional[list[str]] = None,
):
"""Merge values into a LiveKit room's metadata.
async def update_metadata(self, room_name: str, metadata: Optional[Dict] = None):
"""Update a LiveKit room's metadata.
The `room_name` corresponds to the LiveKit room identifier
(i.e. the Room model's UUID as a string).
Raises:
RoomNotFoundException: the room does not exist in LiveKit.
RoomManagementException: the metadata update otherwise fails.
"""
lkapi = utils.create_livekit_client()
try:
response = await lkapi.room.list_rooms(ListRoomsRequest(names=[room_name]))
if not response.rooms:
logger.warning(
"Room %s not found in LiveKit, skipping metadata update",
room_name,
)
raise RoomNotFoundException("Room does not exist")
existing_metadata = json.loads(response.rooms[0].metadata or "{}")
for key in remove_keys or []:
existing_metadata.pop(key, None)
updated_metadata = {**existing_metadata, **(metadata or {})}
await lkapi.room.update_room_metadata(
UpdateRoomMetadataRequest(
room=room_name,
metadata=json.dumps(updated_metadata),
metadata=json.dumps(metadata) if metadata is not None else "",
)
)
@@ -34,8 +34,10 @@ def mediator(mock_worker_service):
return WorkerServiceMediator(mock_worker_service)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
def test_start_recording_success(mock_update_metadata, mediator, mock_worker_service):
@mock.patch("core.utils.update_room_metadata")
def test_start_recording_success(
mock_update_room_metadata, mediator, mock_worker_service
):
"""Test successful recording start"""
# Setup
worker_id = "test-worker-123"
@@ -58,7 +60,7 @@ def test_start_recording_success(mock_update_metadata, mediator, mock_worker_ser
assert mock_recording.worker_id == worker_id
assert mock_recording.status == RecordingStatusChoices.ACTIVE
mock_update_metadata.assert_called_once_with(
mock_update_room_metadata.assert_called_once_with(
str(mock_recording.room.id),
{"recording_mode": mock_recording.mode, "recording_status": "starting"},
)
@@ -67,9 +69,9 @@ def test_start_recording_success(mock_update_metadata, mediator, mock_worker_ser
@pytest.mark.parametrize(
"error_class", [WorkerRequestError, WorkerConnectionError, WorkerResponseError]
)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_mediator_start_recording_worker_errors(
mock_update_metadata, mediator, mock_worker_service, error_class
mock_update_room_metadata, mediator, mock_worker_service, error_class
):
"""Test handling of various worker errors during start"""
# Setup
@@ -87,7 +89,7 @@ def test_mediator_start_recording_worker_errors(
assert mock_recording.status == RecordingStatusChoices.FAILED_TO_START
assert mock_recording.worker_id is None
mock_update_metadata.assert_not_called()
mock_update_room_metadata.assert_not_called()
@pytest.mark.parametrize(
@@ -101,9 +103,9 @@ def test_mediator_start_recording_worker_errors(
RecordingStatusChoices.ABORTED,
],
)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_mediator_start_recording_from_forbidden_status(
mock_update_metadata, mediator, mock_worker_service, status
mock_update_room_metadata, mediator, mock_worker_service, status
):
"""Test handling of various worker errors during start"""
# Setup
@@ -117,7 +119,7 @@ def test_mediator_start_recording_from_forbidden_status(
mock_recording.refresh_from_db()
assert mock_recording.status == status
mock_update_metadata.assert_not_called()
mock_update_room_metadata.assert_not_called()
def test_mediator_stop_recording_success(mediator, mock_worker_service):
@@ -20,9 +20,8 @@ from core.services.livekit_events import (
api,
)
from core.services.lobby import LobbyService
from core.services.room_management import RoomManagementException
from core.services.telephony import TelephonyException, TelephonyService
from core.utils import NotificationError
from core.utils import MetadataUpdateException, NotificationError
pytestmark = pytest.mark.django_db
@@ -71,9 +70,9 @@ def test_initialization(
),
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_success(
mock_update_metadata, mock_notify, mode, notification_type, service
mock_update_room_metadata, mock_notify, mode, notification_type, service
):
"""Should successfully stop recording and notifies all participant."""
@@ -87,8 +86,8 @@ def test_handle_egress_ended_success(
mock_notify.assert_called_once_with(
room_name=str(recording.room.id), notification_data={"type": notification_type}
)
mock_update_metadata.assert_called_once_with(
str(recording.room.id), remove_keys=["recording_mode", "recording_status"]
mock_update_room_metadata.assert_called_once_with(
str(recording.room.id), {}, ["recording_mode", "recording_status"]
)
recording.refresh_from_db()
@@ -105,9 +104,9 @@ def test_handle_egress_ended_success(
(EgressStatus.EGRESS_ABORTED, "aborted"),
),
)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_updated_success(
mock_update_metadata, egress_status, status, service
mock_update_room_metadata, egress_status, status, service
):
"""Should successfully update room's metadata."""
@@ -118,7 +117,7 @@ def test_handle_egress_updated_success(
service._handle_egress_updated(mock_data)
mock_update_metadata.assert_called_once_with(
mock_update_room_metadata.assert_called_once_with(
str(recording.room.id), {"recording_status": status}
)
@@ -130,9 +129,9 @@ def test_handle_egress_updated_success(
EgressStatus.EGRESS_LIMIT_REACHED,
),
)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_updated_non_handled(
mock_update_metadata, egress_status, service
mock_update_room_metadata, egress_status, service
):
"""Should ignore certain egress status and don't trigger metadata updates."""
@@ -143,7 +142,7 @@ def test_handle_egress_updated_non_handled(
service._handle_egress_updated(mock_data)
mock_update_metadata.assert_not_called()
mock_update_room_metadata.assert_not_called()
@pytest.mark.parametrize(
@@ -154,9 +153,9 @@ def test_handle_egress_updated_non_handled(
),
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_metadata_update_fails(
mock_update_metadata, mock_notify, mode, notification_type, service
mock_update_room_metadata, mock_notify, mode, notification_type, service
):
"""Should successfully stop and save recording when metadata's update fails."""
@@ -165,7 +164,7 @@ def test_handle_egress_ended_metadata_update_fails(
mock_data.egress_info.egress_id = recording.worker_id
mock_data.egress_info.status = EgressStatus.EGRESS_LIMIT_REACHED
mock_update_metadata.side_effect = RoomManagementException("Error notifying")
mock_update_room_metadata.side_effect = MetadataUpdateException("Error notifying")
service._handle_egress_ended(mock_data)
@@ -179,9 +178,9 @@ def test_handle_egress_ended_metadata_update_fails(
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_notification_fails(
mock_update_metadata, mock_notify, service
mock_update_room_metadata, mock_notify, service
):
"""Should raise ActionFailedError when notification fails but still stop recording."""
@@ -201,15 +200,15 @@ def test_handle_egress_ended_notification_fails(
recording.refresh_from_db()
assert recording.status == "stopped"
mock_update_metadata.assert_called_once_with(
str(recording.room.id), remove_keys=["recording_mode", "recording_status"]
mock_update_room_metadata.assert_called_once_with(
str(recording.room.id), {}, ["recording_mode", "recording_status"]
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_recording_not_found(
mock_update_metadata, mock_notify, service
mock_update_room_metadata, mock_notify, service
):
"""Should raise ActionFailedError when recording doesn't exist."""
@@ -224,16 +223,16 @@ def test_handle_egress_ended_recording_not_found(
service._handle_egress_ended(mock_data)
mock_notify.assert_not_called()
mock_update_metadata.assert_not_called()
mock_update_room_metadata.assert_not_called()
recording.refresh_from_db()
assert recording.status == "active"
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_recording_not_active(
mock_update_metadata, mock_notify, service
mock_update_room_metadata, mock_notify, service
):
"""Should ignore non-active recordings."""
@@ -245,8 +244,8 @@ def test_handle_egress_ended_recording_not_active(
service._handle_egress_ended(mock_data)
mock_notify.assert_not_called()
mock_update_metadata.assert_called_once_with(
str(recording.room.id), remove_keys=["recording_mode", "recording_status"]
mock_update_room_metadata.assert_called_once_with(
str(recording.room.id), {}, ["recording_mode", "recording_status"]
)
recording.refresh_from_db()
@@ -254,9 +253,9 @@ def test_handle_egress_ended_recording_not_active(
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_recording_not_limit_reached(
mock_update_metadata, mock_notify, service
mock_update_room_metadata, mock_notify, service
):
"""Should ignore egress non-limit-reached statuses."""
@@ -268,16 +267,16 @@ def test_handle_egress_ended_recording_not_limit_reached(
service._handle_egress_ended(mock_data)
mock_notify.assert_not_called()
mock_update_metadata.assert_called_once_with(
str(recording.room.id), remove_keys=["recording_mode", "recording_status"]
mock_update_room_metadata.assert_called_once_with(
str(recording.room.id), {}, ["recording_mode", "recording_status"]
)
assert recording.status == "stopped"
@mock.patch("core.services.livekit_events.MetadataCollectorService")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_calls_metadata_collector_stop_when_conditions_are_met(
mock_update_metadata, mock_collector_class, service, settings
mock_update_room_metadata, mock_collector_class, service, settings
):
"""Should call MetadataCollectorService.stop when it exists."""
settings.METADATA_COLLECTOR_ENABLED = True
@@ -307,7 +306,7 @@ def test_handle_egress_ended_calls_metadata_collector_stop_when_conditions_are_m
],
)
@mock.patch("core.services.livekit_events.MetadataCollectorService")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_does_not_call_metadata_collector_stop_when_conditions_not_met(
_, mock_collector_class, metadata_enabled, options, service, settings
): # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -336,7 +335,7 @@ def test_handle_egress_ended_does_not_call_metadata_collector_stop_when_conditio
"notify_external_services"
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
@pytest.mark.parametrize(
"egress_status",
[EgressStatus.EGRESS_COMPLETE, EgressStatus.EGRESS_LIMIT_REACHED],
@@ -346,7 +345,7 @@ def test_handle_egress_ended_does_not_call_metadata_collector_stop_when_conditio
[(True, "notification_succeeded"), (False, "saved")],
)
def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913
mock_update_metadata,
mock_update_room_metadata,
mock_notify,
mock_notify_external_services,
notify_return_value,
@@ -379,7 +378,7 @@ def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913
"notify_external_services"
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
@pytest.mark.parametrize(
"egress_status, expected_status",
[
@@ -388,7 +387,7 @@ def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913
],
)
def test_handle_egress_ended_does_not_finalize_when_webhooks_enabled( # noqa: PLR0913
mock_update_metadata,
mock_update_room_metadata,
mock_notify,
mock_notify_external_services,
egress_status,
@@ -425,9 +424,9 @@ def test_handle_egress_ended_does_not_finalize_when_webhooks_enabled( # noqa: P
EgressStatus.EGRESS_ABORTED,
],
)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_does_not_save_on_wrong_status(
mock_update_metadata, egress_status, service, settings
mock_update_room_metadata, egress_status, service, settings
):
"""Shouldn't save on invalid status."""
settings.RECORDING_STORAGE_EVENT_ENABLE = False
@@ -446,9 +445,9 @@ def test_handle_egress_ended_does_not_save_on_wrong_status(
@pytest.mark.parametrize(
"status", ["failed_to_start", "aborted", "failed_to_stop", "saved", "initiated"]
)
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
@mock.patch("core.utils.update_room_metadata")
def test_handle_egress_ended_ignores_non_savable_recording(
mock_update_metadata, status, service, settings
mock_update_room_metadata, status, service, settings
):
"""Should handle non-savable recordings idempotently without raising.
+52
View File
@@ -31,6 +31,7 @@ from livekit.api import ( # pylint: disable=E0611
LiveKitAPI,
SendDataRequest,
TwirpError,
UpdateRoomMetadataRequest,
VideoGrants,
)
@@ -257,6 +258,57 @@ async def notify_participants(room_name: str, notification_data: dict):
await lkapi.aclose()
class MetadataUpdateException(Exception):
"""Room's metadata update fails."""
@async_to_sync
async def update_room_metadata(
room_name: str, metadata: dict, remove_keys: Optional[list[str]] = None
):
"""Update LiveKit room metadata by merging new values with existing metadata.
Args:
room_name: Name of the room to update
metadata: Dictionary of metadata key-values to add/update
remove_keys: Optional list of keys to remove from existing metadata.
"""
lkapi = create_livekit_client()
try:
response = await lkapi.room.list_rooms(
ListRoomsRequest(
names=[room_name],
)
)
if not response.rooms:
return
room = response.rooms[0]
existing_metadata = json.loads(room.metadata) if room.metadata else {}
if remove_keys:
for key in remove_keys:
existing_metadata.pop(key, None)
updated_metadata = {**existing_metadata, **metadata}
await lkapi.room.update_room_metadata(
UpdateRoomMetadataRequest(
room=room_name, metadata=json.dumps(updated_metadata).encode("utf-8")
)
)
except TwirpError as e:
raise MetadataUpdateException(
f"Failed to update metadata for room {room_name}: {e}"
) from e
finally:
await lkapi.aclose()
ALPHANUMERIC_CHARSET = string.ascii_letters + string.digits
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.24.0"
version = "1.23.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1187,7 +1187,7 @@ wheels = [
[[package]]
name = "meet"
version = "1.24.0"
version = "1.23.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+26 -32
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.24.0",
"version": "1.23.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.24.0",
"version": "1.23.0",
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
@@ -15,11 +15,11 @@
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.35",
"@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0",
"@tanstack/react-query": "5.101.1",
"@tanstack/react-query": "5.101.0",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.1.2",
"derive-valtio": "0.2.0",
@@ -29,8 +29,8 @@
"i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.20.0",
"posthog-js": "1.395.0",
"livekit-client": "2.19.2",
"posthog-js": "1.391.2",
"react": "18.3.1",
"react-aria": "3.50.0",
"react-aria-components": "1.19.0",
@@ -1028,9 +1028,9 @@
"license": "Apache-2.0"
},
"node_modules/@livekit/protocol": {
"version": "1.46.6",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.46.6.tgz",
"integrity": "sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==",
"version": "1.45.8",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.45.8.tgz",
"integrity": "sha512-Q+l57E7w/xxOBFVWzdX5rkAZO7ffyF+rlDzNUYq2SU114+5aTyCq+PK4unaEVDNd4952Af7wteKr3sOgasGuaA==",
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protobuf": "^1.10.0"
@@ -1049,18 +1049,12 @@
"livekit-client": "^1.12.0 || ^2.1.0"
}
},
"node_modules/@livekit/track-processors/node_modules/@mediapipe/tasks-vision": {
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.14.tgz",
"integrity": "sha512-vOifgZhkndgybdvoRITzRkIueWWSiCKuEUXXK6Q4FaJsFvRJuwgg++vqFUMlL0Uox62U5aEXFhHxlhV7Ja5e3Q==",
"license": "Apache-2.0"
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.35",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.35.tgz",
"integrity": "sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==",
"license": "Apache-2.0"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
@@ -2421,9 +2415,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.101.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.1.tgz",
"integrity": "sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==",
"version": "5.101.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
"integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2442,12 +2436,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.101.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.1.tgz",
"integrity": "sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==",
"version": "5.101.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
"integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.101.1"
"@tanstack/query-core": "5.101.0"
},
"funding": {
"type": "github",
@@ -8125,13 +8119,13 @@
"license": "MIT"
},
"node_modules/livekit-client": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.20.0.tgz",
"integrity": "sha512-RIJcpvBmOmwz3jTj3rmdY6Dzr55HrhcaJjMgY+HSmoEM+yIRyA40m7r8UKv0hnZWM3z/AYhP1q8C8ciz5UWFKQ==",
"version": "2.19.2",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.2.tgz",
"integrity": "sha512-Kvk07QYDWRAbmYNLRll04ZIuxMQobW/oLPYnmR1kCy8GGHpU0gqyHf704Rz+29zfy8IJZRjKqeVbzGSKn9sumw==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/mutex": "1.1.1",
"@livekit/protocol": "1.46.6",
"@livekit/protocol": "1.45.8",
"events": "^3.3.0",
"jose": "^6.1.0",
"loglevel": "^1.9.2",
@@ -9136,13 +9130,13 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.395.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.395.0.tgz",
"integrity": "sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==",
"version": "1.391.2",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.391.2.tgz",
"integrity": "sha512-q0DZN6ljchSnAFJIXf+sQFTPlsLjTlRa+TvrL+QRb6413BGtib/MNiQy1bnwLKt8KR+f6xJYvkqdLyty9s4Aww==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@posthog/core": "^1.38.0",
"@posthog/types": "^1.391.1",
"@posthog/core": "^1.35.3",
"@posthog/types": "^1.390.2",
"core-js": "^3.38.1",
"dompurify": "^3.3.2",
"fflate": "^0.4.8",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.24.0",
"version": "1.23.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -22,11 +22,11 @@
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.35",
"@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0",
"@tanstack/react-query": "5.101.1",
"@tanstack/react-query": "5.101.0",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.1.2",
"derive-valtio": "0.2.0",
@@ -36,8 +36,8 @@
"i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.20.0",
"posthog-js": "1.395.0",
"livekit-client": "2.19.2",
"posthog-js": "1.391.2",
"react": "18.3.1",
"react-aria": "3.50.0",
"react-aria-components": "1.19.0",
@@ -13,7 +13,8 @@ import { useRoomContext } from '@livekit/components-react'
import { useRoomData } from './useRoomData'
/**
* The subset of LiveKit's room metadata this hook actually uses.
* Shape of the LiveKit room metadata blob pushed by the backend.
* Matches RoomManagement.update_metadata → {"configuration": room.configuration}
*/
type RoomLiveKitMetadata = {
configuration?: RoomConfiguration
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.27
version: 0.0.26
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.24.0",
"version": "1.23.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.24.0",
"version": "1.23.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.24.0",
"version": "1.23.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.24.0",
"version": "1.23.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.24.0",
"version": "1.23.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.24.0",
"version": "1.23.0",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.24.0"
version = "1.23.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
+3 -18
View File
@@ -18,12 +18,7 @@ from requests import exceptions
from summary.core.analytics import MetadataManager, get_analytics
from summary.core.config import get_settings
from summary.core.docs_service import create_document_in_lasuite_docs
from summary.core.file_service import (
CorruptedAudioFile,
FileService,
FileServiceException,
TranscribeError,
)
from summary.core.file_service import FileService, FileServiceException, TranscribeError
from summary.core.llm_service import LLMException, LLMObservability, LLMService
from summary.core.locales import get_locale
from summary.core.models import (
@@ -152,20 +147,10 @@ def transcribe_audio(
# Mimic OpenAI's timeout settings
timeout=(60, 10 * 60),
)
if res.status_code == 400:
logger.info(
"WhisperX transcription failed, "
"likely due to a corrupted audio file: %s",
res.text,
)
raise CorruptedAudioFile("WhisperX coudln't decode the audio file.")
try:
res.raise_for_status()
except requests.exceptions.HTTPError:
logger.exception("WhisperX transcription failed")
# We reraise the error so that it can be retried by celery
raise
except requests.exceptions.HTTPError as e:
raise RuntimeError("WhisperX transcription failed, %s", res.text) from e
transcription_json: dict[str, Any] = res.json()
# We remove the "usage" key from the transcription_json dictionary
-6
View File
@@ -41,12 +41,6 @@ class NoAudioInFileError(TranscribeError):
error_code = "no_audio_in_file"
class CorruptedAudioFile(TranscribeError):
"""Raised when a media file does not contain any audio."""
error_code = "corrupted_audio_file"
def _get_duration_from_packets(local_path: Path) -> float:
"""Estimate duration from audio packet timestamps."""
# Run ffprobe to inspect the first audio stream in the file.
+1 -7
View File
@@ -96,13 +96,7 @@ class TranscribeWebhookFailurePayload(BaseWebhook):
# we authorized any other string than the one in the literal
# to avoid causing a breaking change on the client side
error_code: (
Literal[
"unknown_error",
"no_audio_in_file",
"media_duration_too_long",
"corrupted_audio_file",
]
| str
Literal["unknown_error", "no_audio_in_file", "media_duration_too_long"] | str
) = Field(title="Error code", description="The error code.")
+1 -1
View File
@@ -275,7 +275,7 @@ def _build_participant_timelines(
def _build_speaker_timelines(transcription: Any) -> dict[str, list[Interval]]:
"""Build interval timelines from WhisperX transcription segments."""
intervals: dict[str, list[Interval]] = {}
segments = transcription.get("segments") or []
segments = transcription.segments if hasattr(transcription, "segments") else []
max_word_duration = settings.resolve_speaker_identities_max_word_duration
for segment in segments:
+23 -20
View File
@@ -1,6 +1,7 @@
"""Tests for the speaker-to-user assignment service."""
import math
from dataclasses import dataclass, field
from datetime import datetime
from summary.core import user_assign
@@ -16,9 +17,11 @@ from summary.core.user_assign import (
)
def make_transcription(segments: list | None = None) -> dict:
"""Build a WhisperX transcription dict, matching WhisperXResponse.model_dump()."""
return {"segments": segments or []}
@dataclass
class FakeTranscription:
"""Mimics the OpenAI Transcription pydantic model for testing."""
segments: list = field(default_factory=list)
RECORDING_START = datetime.fromisoformat("2026-03-17T15:30:33.000001")
@@ -65,7 +68,7 @@ METADATA_SINGLE_USER = {
],
}
DIARIZATION_SINGLE_SPEAKER = make_transcription(
DIARIZATION_SINGLE_SPEAKER = FakeTranscription(
segments=[
{
"start": 1.363,
@@ -179,7 +182,7 @@ class TestBuildSpeakerTimelines:
def test_segment_without_words_falls_back_to_segment_bounds(self):
"""Segments missing a `words` key use the segment start/end as one interval."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[{"start": 1.5, "end": 3.5, "speaker": "SPEAKER_00"}],
)
result = _build_speaker_timelines(transcription)
@@ -187,7 +190,7 @@ class TestBuildSpeakerTimelines:
def test_segment_with_only_none_word_timestamps_falls_back(self):
"""If every word has None start/end, fall back to segment bounds."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 1.0,
@@ -205,7 +208,7 @@ class TestBuildSpeakerTimelines:
def test_short_words_only_uses_segment_start_and_last_word_end(self):
"""With no overly long words, the interval runs segment start to end."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 1.0,
@@ -228,7 +231,7 @@ class TestBuildSpeakerTimelines:
max_word_duration = (
user_assign.settings.resolve_speaker_identities_max_word_duration
)
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 0.0,
@@ -249,7 +252,7 @@ class TestBuildSpeakerTimelines:
def test_long_word_in_middle_splits_segment(self):
"""Short words around a long word produce two intervals (before-cap + after)."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 0.0,
@@ -272,7 +275,7 @@ class TestBuildSpeakerTimelines:
def test_tail_word_is_capped_at_max_duration(self):
"""The trailing word's end is capped at word.start + max_word_duration."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 0.0,
@@ -299,7 +302,7 @@ class TestBuildSpeakerTimelines:
update={"resolve_speaker_identities_enable_split_on_words": False},
),
)
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 0.0,
@@ -319,7 +322,7 @@ class TestBuildSpeakerTimelines:
def test_multiple_speakers_keep_separate_timelines(self):
"""Segments from different speakers populate independent timeline entries."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{
"start": 0.0,
@@ -399,7 +402,7 @@ class TestResolveSpeakerIdentities:
],
"participants": [{"participantId": "user-a", "name": "Shared Mic"}],
}
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 1.0, "end": 3.0, "speaker": "SPEAKER_00"},
{"start": 5.0, "end": 7.0, "speaker": "SPEAKER_01"},
@@ -442,7 +445,7 @@ class TestResolveSpeakerIdentities:
{"participantId": "user-b", "name": "Bob"},
],
}
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 1.5, "end": 3.5, "speaker": "SPEAKER_00"},
{"start": 5.5, "end": 7.5, "speaker": "SPEAKER_01"},
@@ -490,7 +493,7 @@ class TestResolveSpeakerIdentities:
{"participantId": "user-b", "name": "Bob"},
],
}
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 1.5, "end": 5.5, "speaker": "SPEAKER_00"},
{"start": 4.0, "end": 7.5, "speaker": "SPEAKER_01"},
@@ -526,7 +529,7 @@ class TestResolveSpeakerIdentities:
],
"participants": [{"participantId": "user-a", "name": "Brief User"}],
}
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 1.0, "end": 10.0, "speaker": "SPEAKER_00"},
],
@@ -558,7 +561,7 @@ class TestResolveSpeakerIdentities:
],
"participants": [{"participantId": "user-a", "name": "Early User"}],
}
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 0.0, "end": 3.0, "speaker": "SPEAKER_00"},
],
@@ -573,7 +576,7 @@ class TestResolveSpeakerIdentities:
"""No segments produces empty result."""
result = resolve_speaker_identities(
METADATA_SINGLE_USER,
make_transcription(segments=[]),
FakeTranscription(segments=[]),
RECORDING_START,
RECORDING_END,
)
@@ -581,7 +584,7 @@ class TestResolveSpeakerIdentities:
def test_segment_without_speaker_ignored(self):
"""Segments missing speaker key are skipped."""
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 1.0, "end": 3.0, "text": "no speaker"},
],
@@ -605,7 +608,7 @@ class TestResolveSpeakerIdentities:
],
"participants": [{"participantId": "user-a", "name": "Still Talking"}],
}
transcription = make_transcription(
transcription = FakeTranscription(
segments=[
{"start": 2.0, "end": 9.0, "speaker": "SPEAKER_00"},
],