Compare commits

..

17 Commits

Author SHA1 Message Date
Cyril 839a8f1c71 💄(frontend) add consistent spacing to picture-in-picture tiles
Add padding to StageFrame and bigger gaps so PiP tiles aren't flush with window.
2026-07-10 08:45:53 +02:00
Cyril 40ef420cda (frontend) use focus layout for solo PiP screen sharing
Solo cam in screen share PiP is now a BR thumbnail over shared screen
2026-07-10 08:45:51 +02:00
Cyril d995bd9041 (frontend) prioritize screen share in picture-in-picture layout
Show screen share large with cameras in a top row; match main room focus.
2026-07-10 08:45:51 +02:00
lebaudantoine 8ac4c2409d 💚(mail) update the node version for the mail building step
Broken with the dependencies update.
2026-07-09 16:22:54 +02:00
lebaudantoine e47ad42656 ⬆️(mail) update mjml to v5 and @html-to/text-cli
@html-to/text-cli was containing CVEs reported by the audit.
2026-07-09 16:22:54 +02:00
Florent Chehab 2509452c52 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1
Add a deprecation warning when SUMMARY_SERVICE_VERSION=1 and
an endpoint is configured meaning summary service is in use.
2026-07-09 16:14:23 +02:00
Florent Chehab 1022780027 📝(upgrade) add note related to summary v1 removal
Add upgrade notes related to the summary changes in v1.23.0
2026-07-09 15:49:53 +02:00
Florent Chehab 4a1a04f86e 🔖(minor) bump release to 1.23.0 2026-07-08 10:07:09 +02:00
Florent Chehab 8702638cf3 (release) uv lock agents
In release script also run uv lock for agents.
2026-07-08 10:06:53 +02:00
Florent Chehab f88c0307ea 🐛(summary) do not save null emails in analytics
In case the email is not provided we should not update
the analytics email value.
2026-07-08 02:09:30 +02:00
Florent Chehab e7f15b50ff (summary) more precise analytics events
* Specific transcript and summary events
* Improve observability on summary tasks
2026-07-08 02:09:30 +02:00
snyk-bot 75ba2ff146 🔒️(frontend) update docker image to nginx-unprivileged:1.30.3-alpine3.23
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE323-EXPAT-17675120
- https://snyk.io/vuln/SNYK-ALPINE323-EXPAT-17675112
- https://snyk.io/vuln/SNYK-ALPINE323-EXPAT-17675114
- https://snyk.io/vuln/SNYK-ALPINE323-EXPAT-17675124
- https://snyk.io/vuln/SNYK-ALPINE323-EXPAT-17675125
2026-07-08 01:48:59 +02:00
lebaudantoine bbe2a32efc ⬆️(frontend) update the frontend build image to Node 22
Bump the Node.js version used in the frontend build image from
Node 20 to Node 22.
2026-07-08 01:39:20 +02:00
snyk-bot 86fff16eed ⬆️(frontend) upgrade @tanstack/react-query from 5.100.14 to 5.101.0
Snyk has created this PR to upgrade @tanstack/react-query from 5.100.14 to 5.101.0.
2026-07-08 01:02:50 +02:00
snyk-bot 195d2b5006 ⬆️(frontend) upgrade posthog-js from 1.386.5 to 1.387.0
Snyk has created this PR to upgrade posthog-js from 1.386.5 to 1.387.0.
2026-07-08 00:56:41 +02:00
snyk-bot 30cf264276 ⬆️(frontend) upgrade livekit-client from 2.19.0 to 2.19.2
Snyk has created this PR to upgrade livekit-client from 2.19.0 to 2.19.2.
2026-07-08 00:48:27 +02:00
lebaudantoine 945d779b45 ♻️(frontend) wrap MuteEveryoneButton with AdminOrOwnerOnly
Avoids mounting the button's hooks and rendering its logic for users who
are not admin or owner, which previously happened on every re-render
of the parent.
2026-07-07 19:11:41 +02:00
32 changed files with 2245 additions and 1065 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: "18"
node-version: "22"
- name: Restore the mail templates
uses: actions/cache@v5
+14
View File
@@ -8,6 +8,13 @@ and this project adheres to
## [Unreleased]
### Changed
- 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1
- ⬆️(mail) update mjml to v5 and @html-to/text-cli
## [1.23.0] - 2026-07-08
### Added
- ✨(backend) extend analytics module to support feature flags
@@ -23,6 +30,12 @@ and this project adheres to
- ♻️(backend) refactor analytics backend from Protocol to abstract class
- 🔥(summary) remove call to summary enabled feature flag
- ♻️(frontend) wrap MuteEveryoneButton with AdminOrOwnerOnly
- ⬆️(frontend) upgrade livekit-client from 2.19.0 to 2.19.2
- ⬆️(frontend) upgrade posthog-js from 1.386.5 to 1.387.0
- ⬆️(frontend) upgrade @tanstack/react-query from 5.100.14 to 5.101.0
- ⬆️(frontend) update the frontend build image to Node 22
- 🔒️(frontend) update docker image to nginx-unprivileged:1.30.3-alpine3.23
- ✨(summary) more precise analytics events
### Fixed
@@ -42,6 +55,7 @@ and this project adheres to
- ✨(backend) add fallback to save recordings without S3/MinIO webhooks
- 🩹(frontend) enable screen share button in PiP #1458
- 🐛(backend) support unencoded S3 notification object keys #1455
- ✨(frontend) prioritize screen share in picture-in-picture layout #1467
### Changed
+12
View File
@@ -15,3 +15,15 @@ the following command inside your docker container:
(Note : in your development environment, you can `make migrate`.)
## [Unreleased]
## v1.23.0
As part of the 1.23.0 release, the legacy `api/v1` implementation has been removed from the _experimental_ Summary service and Meet has been migrated to the new `api/v2`.
**To avoid a breaking change, the Meet backend continues to use the Summary service's v1-compatible API format by default (`SUMMARY_SERVICE_VERSION` setting defaults to `1`).**
If you are deploying both Meet and Summary from this repository, you must configure the Meet backend to use the v2 API by setting the following environment variable `SUMMARY_SERVICE_VERSION=2`.
If you are upgrading only the Meet deployment while keeping an older Summary v1 compatible deployment, no action is required, as the v1-compatible API remains the default.
Note that we plan on removing the legacy `v1` summary compatibility in a future major version. If you have your own implementation for the summary service, we recommend updating its API contract and setting `SUMMARY_SERVICE_VERSION=2`.
+6
View File
@@ -113,6 +113,12 @@ update_python_version "summary"
# Update agents pyproject.toml
update_python_version "agents"
# Run uv lock in agents
print_info "Running uv lock in agents..."
cd "src/agents"
uv lock
cd -
# Update CHANGELOG
print_info "Updating CHANGELOG..."
+2 -2
View File
@@ -1,5 +1,5 @@
# ---- Front-end image ----
FROM node:20-alpine AS frontend-deps
FROM node:22-alpine AS frontend-deps
WORKDIR /home/frontend/
@@ -54,7 +54,7 @@ RUN npx webpack --mode production
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.23 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.30.3-alpine3.23 AS frontend-production
USER root
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.22.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.22.0"
version = "1.23.0"
source = { virtual = "." }
dependencies = [
{ name = "livekit-agents" },
+15
View File
@@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
# pylint: disable=too-many-lines
import json
import warnings
from os import path
from socket import gethostbyname, gethostname
@@ -1128,6 +1129,20 @@ class Base(Configuration):
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
)
if (
cls.SUMMARY_SERVICE_VERSION == 1
and cls.SUMMARY_SERVICE_ENDPOINT is not None
):
warnings.warn(
"SUMMARY_SERVICE_VERSION=1 is deprecated. "
"The legacy v1 API has been removed from the experimental "
"Summary service. Please update your Summary service deployment to "
"the v2 API and set SUMMARY_SERVICE_VERSION=2.",
# We use UserWarning to make sure it shows up in production deployment
UserWarning,
stacklevel=2,
)
# The SENTRY_DSN setting should be available to activate sentry for an environment
if cls.SENTRY_DSN is not None:
sentry_sdk.init(
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.22.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.22.0"
version = "1.23.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine AS frontend-deps
FROM node:22-alpine AS frontend-deps
USER node
@@ -39,7 +39,7 @@ ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.23 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.30.3-alpine3.23 AS frontend-production
USER root
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.22.0",
"version": "1.23.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.22.0",
"version": "1.23.0",
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
@@ -18,7 +18,7 @@
"@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0",
"@tanstack/react-query": "5.100.14",
"@tanstack/react-query": "5.101.0",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.1.2",
"derive-valtio": "0.2.0",
@@ -28,8 +28,8 @@
"i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.19.0",
"posthog-js": "1.386.5",
"livekit-client": "2.19.2",
"posthog-js": "1.387.0",
"react": "18.3.1",
"react-aria": "3.49.0",
"react-aria-components": "1.18.0",
@@ -2357,9 +2357,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
"integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
"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",
@@ -2378,12 +2378,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
"integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
"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.100.14"
"@tanstack/query-core": "5.101.0"
},
"funding": {
"type": "github",
@@ -8035,9 +8035,9 @@
"license": "MIT"
},
"node_modules/livekit-client": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.19.0.tgz",
"integrity": "sha512-aolY1XDAtx0nHKBNm29W9OhzBnSz1CP5kq3phvRhFfi1NbvMXs8tcACjAkZTnIKgihkp+BiJScZZ3tZv0Gz8sA==",
"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",
@@ -9033,13 +9033,13 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.386.5",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.386.5.tgz",
"integrity": "sha512-ASejQQf5Xw0XolMwH/KCLZlZtoyLK6VsvORwGagAtfa8/ElIOF76BMQspkDsRTybEI+uzHqRDm2m/na1Dki2mA==",
"version": "1.387.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.387.0.tgz",
"integrity": "sha512-Pv1jUMySMN62zoAxdJBJPV8n62lkHdjuWhpeU7izczc5Dqbx3hhqO2hkrNTI8Yx1ezmWk2qUHZs03FuOBubdFQ==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@posthog/core": "^1.32.3",
"@posthog/types": "^1.386.3",
"@posthog/core": "^1.33.0",
"@posthog/types": "^1.387.0",
"core-js": "^3.38.1",
"dompurify": "^3.3.2",
"fflate": "^0.4.8",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.22.0",
"version": "1.23.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -25,7 +25,7 @@
"@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0",
"@tanstack/react-query": "5.100.14",
"@tanstack/react-query": "5.101.0",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.1.2",
"derive-valtio": "0.2.0",
@@ -35,8 +35,8 @@
"i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.19.0",
"posthog-js": "1.386.5",
"livekit-client": "2.19.2",
"posthog-js": "1.387.0",
"react": "18.3.1",
"react-aria": "3.49.0",
"react-aria-components": "1.18.0",
@@ -26,6 +26,7 @@ export const PipFocusLayout = memo(
<ParticipantTile
key={getTrackKey(mainTrack)}
trackRef={mainTrack}
disableTileControls
/>
</MainSlot>
)}
@@ -34,6 +35,7 @@ export const PipFocusLayout = memo(
<ParticipantTile
key={getTrackKey(thumbnailTrack)}
trackRef={thumbnailTrack}
disableTileControls
/>
</Thumbnail>
)}
@@ -48,9 +50,10 @@ const FocusContainer = styled('div', {
position: 'relative',
width: '100%',
height: '100%',
borderRadius: '4px',
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
boxSizing: 'border-box',
},
})
@@ -58,6 +61,8 @@ const MainSlot = styled('div', {
base: {
width: '100%',
height: '100%',
borderRadius: '8px',
overflow: 'hidden',
'& .lk-participant-media-video': {
objectFit: 'contain',
},
@@ -67,13 +72,13 @@ const MainSlot = styled('div', {
const Thumbnail = styled('div', {
base: {
position: 'absolute',
right: '1rem',
bottom: '1rem',
right: '1.25rem',
bottom: '1.25rem',
width: '42%',
maxWidth: '220px',
minWidth: '140px',
aspectRatio: '16 / 9',
borderRadius: '4px',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: 'md',
zIndex: 2,
@@ -41,7 +41,7 @@ export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
<GridContainer ref={containerRef} style={gridStyle}>
{tracks.map((track, index) => (
<GridCell key={getTrackKey(track)} style={placements[index]}>
<ParticipantTile trackRef={track} />
<ParticipantTile trackRef={track} disableTileControls />
</GridCell>
))}
</GridContainer>
@@ -54,7 +54,8 @@ const GridContainer = styled('div', {
width: '100%',
height: '100%',
display: 'grid',
gap: '0.25rem',
gap: '0.5rem',
boxSizing: 'border-box',
},
})
@@ -63,7 +64,7 @@ const GridCell = styled('div', {
position: 'relative',
minWidth: 0,
minHeight: 0,
borderRadius: '4px',
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
// Paint on own layer so FLIP transforms don't trigger layout thrash.
@@ -0,0 +1,97 @@
import { memo } from 'react'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { styled } from '@/styled-system/jsx'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { getTrackKey } from '../../utils/pipTrackSelection'
type PipScreenShareLayoutProps = {
screenShareTrack: TrackReferenceOrPlaceholder
cameraTracks: TrackReferenceOrPlaceholder[]
}
/**
* Layout when a screen share is active.
* Camera tiles are shown as a compact row at the top; the screen share occupies
* the remaining space below, much larger than the camera tiles.
*/
export const PipScreenShareLayout = memo(
({ screenShareTrack, cameraTracks }: PipScreenShareLayoutProps) => {
return (
<LayoutContainer>
{cameraTracks.length > 0 && (
<CameraTilesRow>
{cameraTracks.map((track) => (
<CameraTile key={getTrackKey(track)}>
<ParticipantTile trackRef={track} disableTileControls />
</CameraTile>
))}
</CameraTilesRow>
)}
<ScreenShareSlot>
<ParticipantTile trackRef={screenShareTrack} disableTileControls />
</ScreenShareSlot>
</LayoutContainer>
)
}
)
PipScreenShareLayout.displayName = 'PipScreenShareLayout'
const LayoutContainer = styled('div', {
base: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
boxSizing: 'border-box',
overflow: 'hidden',
},
})
const CameraTilesRow = styled('div', {
base: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
gap: '0.5rem',
flexShrink: 0,
height: '22%',
minHeight: '60px',
maxHeight: '120px',
},
})
const CameraTile = styled('div', {
base: {
position: 'relative',
flex: '0 1 auto',
height: '100%',
aspectRatio: '16 / 9',
minWidth: 0,
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
'& .lk-participant-tile': {
width: '100%',
height: '100%',
},
},
})
const ScreenShareSlot = styled('div', {
base: {
position: 'relative',
flex: 1,
minHeight: 0,
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
'& .lk-participant-tile': {
width: '100%',
height: '100%',
},
'& .lk-participant-media-video': {
objectFit: 'contain',
},
},
})
@@ -1,10 +1,11 @@
import { useMemo } from 'react'
import React, { useMemo } from 'react'
import { usePagination, useTracks } from '@livekit/components-react'
import { RoomEvent, Track } from 'livekit-client'
import { styled } from '@/styled-system/jsx'
import { PipFocusLayout } from './PipFocusLayout'
import { PipGridLayout } from './PipGridLayout'
import { PipPagination } from './PipPagination'
import { PipScreenShareLayout } from './PipScreenShareLayout'
import { StageFrame } from './StageFrame'
import { MAX_PIP_TILES } from '../../utils/pipGrid'
import {
@@ -13,9 +14,12 @@ import {
} from '@livekit/components-core'
/**
* PipStage picks between two layouts based on track count:
* - Focus mode (≤ 2 tracks): one main track + one thumbnail overlay.
* - Grid mode (3+ tracks): adaptive tiling.
* PipStage picks between three layouts:
* - Screen share mode (any screen share active):
* small camera tiles in a row at the top, large screen share below.
* - Grid mode (3+ camera tracks, no screen share): adaptive tiling.
* - Focus mode (≤ 2 camera tracks, no screen share): one main track
* + one thumbnail overlay.
*/
export const PipStage = () => {
const tracks = useTracks(
@@ -41,51 +45,55 @@ export const PipStage = () => {
[tracks]
)
// Grid mode order: screen share leads, then cameras (already ordered by
// active speaker via the `ActiveSpeakersChanged` update above).
const gridTracks = useMemo(
() =>
screenShareTrack ? [screenShareTrack, ...cameraTracks] : cameraTracks,
[screenShareTrack, cameraTracks]
)
// Cap camera tiles in screen-share mode. Called unconditionally for hook rules.
const paginatedCameraTracks = usePagination(MAX_PIP_TILES - 1, cameraTracks)
// Cap the grid at MAX_PIP_TILES per page. `usePagination` keeps the visible
// page visually stable (active/recent speakers stay put) via its internal
// `useVisualStableUpdate`. Called unconditionally to respect hook rules.
const pagination = usePagination(MAX_PIP_TILES, gridTracks)
// Cap the grid at MAX_PIP_TILES per page for the non-screenshare grid mode.
const pagination = usePagination(MAX_PIP_TILES, cameraTracks)
if (tracks.length === 0) return null
/**
* The focus layout shows one main track with one thumbnail overlay,
* so it can only fit 2 tracks. Beyond that we switch to the grid.
*/
if (gridTracks.length > 2) {
return (
<StageWrapper>
// Screen share active
if (screenShareTrack) {
// Solo presenter: screen share fills the area, camera as small thumbnail
if (cameraTracks.length <= 1) {
return (
<StageFrame>
<PipGridLayout tracks={pagination.tracks} />
<PipFocusLayout
mainTrack={screenShareTrack}
thumbnailTrack={cameraTracks[0]}
/>
</StageFrame>
<PipPagination
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
nextPage={pagination.nextPage}
prevPage={pagination.prevPage}
)
}
// Multiple cameras: camera row at top, screen share below
return (
<PaginatedStage pagination={paginatedCameraTracks}>
<PipScreenShareLayout
screenShareTrack={screenShareTrack}
cameraTracks={paginatedCameraTracks.tracks}
/>
</StageWrapper>
</PaginatedStage>
)
}
// 3+ camera tracks → adaptive grid
if (cameraTracks.length > 2) {
return (
<PaginatedStage pagination={pagination}>
<PipGridLayout tracks={pagination.tracks} />
</PaginatedStage>
)
}
// ≤ 2 camera tracks → focus layout (main + optional thumbnail)
const localCameraTrack = cameraTracks.find(
(track) => track.participant?.isLocal
)
const remoteCameraTrack = cameraTracks.find(
(track) => !track.participant?.isLocal
)
const mainTrack = screenShareTrack ?? remoteCameraTrack ?? localCameraTrack
const mainTrack = remoteCameraTrack ?? localCameraTrack
const thumbnailTrack =
mainTrack === localCameraTrack ? undefined : localCameraTrack
@@ -96,6 +104,31 @@ export const PipStage = () => {
)
}
type PaginationResult = {
totalPageCount: number
currentPage: number
nextPage: () => void
prevPage: () => void
}
const PaginatedStage = ({
pagination,
children,
}: {
pagination: PaginationResult
children: React.ReactNode
}) => (
<StageWrapper>
<StageFrame>{children}</StageFrame>
<PipPagination
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
nextPage={pagination.nextPage}
prevPage={pagination.prevPage}
/>
</StageWrapper>
)
const StageWrapper = styled('div', {
base: {
display: 'flex',
@@ -25,8 +25,8 @@ const Container = styled('div', {
flex: 1,
minWidth: 0,
minHeight: 0,
marginLeft: '0.5rem',
marginRight: '0.5rem',
padding: '0.5rem',
boxSizing: 'border-box',
borderRadius: '4px',
overflow: 'hidden',
},
@@ -52,6 +52,7 @@ export function TrackRefContextIfNeeded(
interface ParticipantTileExtendedProps extends ParticipantTileProps {
disableMetadata?: boolean
disableTileControls?: boolean
}
export const ParticipantTile: (
@@ -66,6 +67,7 @@ export const ParticipantTile: (
onParticipantClick,
disableSpeakingIndicator,
disableMetadata,
disableTileControls,
...htmlProps
}: ParticipantTileExtendedProps,
ref
@@ -231,7 +233,7 @@ export const ParticipantTile: (
)}
</>
)}
{!disableMetadata && (
{!disableMetadata && !disableTileControls && (
<ParticipantTileFocus
trackRef={trackReference}
hasKeyboardFocus={hasKeyboardFocus}
+1 -1
View File
@@ -6,4 +6,4 @@ DIR_MAILS="../backend/core/templates/mail/html/"
if [ ! -d "${DIR_MAILS}" ]; then
mkdir -p "${DIR_MAILS}";
fi
mjml mjml/*.mjml -o "${DIR_MAILS}";
mjml mjml/*.mjml -o "${DIR_MAILS}" --config.allowIncludes true;
+7 -2
View File
@@ -2,6 +2,11 @@
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<!--
We load django tags here so they appear in the body of the HTML output.
This ensures html-to-text also includes them in the plain text template.
-->
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
<mj-section css-class="wrapper-logo">
<mj-column>
@@ -59,7 +64,7 @@
</mj-column>
</mj-section>
</mj-wrapper>
</mj-body>
<mj-include path="./partial/footer.mjml" />
<mj-include path="./partial/footer.mjml" />
</mj-body>
</mjml>
+4 -9
View File
@@ -1,21 +1,16 @@
<mj-head>
<mj-title>{{ title }}</mj-title>
<mj-preview>
<!--
We load django tags here, in this way there are put within the body in html output
so the html-to-text command includes it within its output
-->
{% load i18n static extra_tags %}
{{ title }}
</mj-preview>
<mj-preview>{{ title }}</mj-preview>
<mj-font name="Roboto" href="https://fonts.bunny.net/css?family=roboto:400,700,900" />
<mj-attributes>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700;900&display=swap" />
<mj-all
font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"
font-size="16px"
line-height="1.5em"
color="#3A3A3A"
/>
<mj-text font-family="Roboto, sans-serif" />
<mj-button font-family="Roboto, sans-serif" />
</mj-attributes>
<mj-style>
/* Reset */
+5
View File
@@ -2,6 +2,11 @@
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<!--
We load django tags here so they appear in the body of the HTML output.
This ensures html-to-text also includes them in the plain text template.
-->
<mj-raw>{% load i18n static extra_tags %}</mj-raw>
<mj-wrapper css-class="wrapper" padding="5px 25px 0px 25px">
<mj-section css-class="wrapper-logo">
<mj-column>
+1904 -942
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "mail_mjml",
"version": "1.22.0",
"version": "1.23.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
"@html-to/text-cli": "0.5.4",
"mjml": "4.18.0"
"@html-to/text-cli": "0.6.0",
"mjml": "5.4.0"
},
"private": true,
"scripts": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.22.0",
"version": "1.23.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.22.0",
"version": "1.23.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.22.0",
"version": "1.23.0",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.22.0"
version = "1.23.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
+13 -14
View File
@@ -59,17 +59,16 @@ async def create_transcribe_task_v2(
]
)
properties = {}
if request.user_email:
properties["$set"] = {"email": request.user_email}
# We track the request, this also properly initializes the user in the
# analytics system, so that later feature flags work properly
analytics.capture(
settings.posthog_event_request,
settings.posthog_transcript_request,
request.user_sub,
properties={
"kind": "transcribe",
"$set": {
"email": request.user_email,
},
},
properties=properties,
)
return TranscribeWebhookPendingPayload(job_id=task.id).model_dump()
@@ -93,15 +92,15 @@ async def create_summarize_task_v2(
# We track the request, this also properly initializes the user in the
# analytics system, so that later feature flags work properly
properties = {}
if request.user_email:
properties["$set"] = {"email": request.user_email}
analytics.capture(
settings.posthog_event_request,
settings.posthog_summary_request,
request.user_sub,
properties={
"kind": "summarize",
"$set": {
"email": request.user_email,
},
},
properties=properties,
)
return SummarizeWebhookPendingPayload(job_id=task.id).model_dump()
+11 -6
View File
@@ -11,7 +11,7 @@ from celery.utils.log import get_task_logger
from posthog import Posthog
from summary.core.config import get_settings
from summary.core.models import TranscribeTaskJob
from summary.core.models import SummarizeTaskJob, TranscribeTaskJob
logger = get_task_logger(__name__)
settings = get_settings()
@@ -109,19 +109,15 @@ class MetadataManager:
"""Check if task_id exists in tasks metadata cache."""
return self._redis.exists(self._get_redis_key(task_id))
def create(self, task_id: str, task_payload: TranscribeTaskJob):
def create(self, task_id: str, task_payload: TranscribeTaskJob | SummarizeTaskJob):
"""Create initial metadata entry for a new task."""
if self._is_disabled or self.has_task_id(task_id):
return
start_time = time.time()
parts = urlsplit(task_payload.cloud_storage_url)
clean_url = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
initial_metadata = {
"start_time": start_time,
"asr_model": settings.whisperx_asr_model,
"retries": 0,
"filename": clean_url,
"sub": task_payload.user_sub,
# avoid None in redis, it shouldn't happen anyway in prod
"email": task_payload.user_email or "",
@@ -129,6 +125,15 @@ class MetadataManager:
"queuing_time": round(start_time - task_payload.received_at.timestamp(), 2),
}
if isinstance(task_payload, TranscribeTaskJob):
parts = urlsplit(task_payload.cloud_storage_url)
clean_url = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
initial_metadata["source_url"] = clean_url
initial_metadata["asr_model"] = settings.whisperx_asr_model
elif isinstance(task_payload, SummarizeTaskJob):
initial_metadata["content_length"] = len(task_payload.content)
initial_metadata["llm_model"] = settings.llm_model
self._save_metadata(task_id, initial_metadata)
def retry(self, task_id):
+26 -5
View File
@@ -542,28 +542,28 @@ def process_audio_transcribe_v2_task(
call_webhook_v2_task.apply_async(
args=[success_payload.model_dump(), payload.tenant_id]
)
metadata_manager.capture(job_id, settings.posthog_event_success)
metadata_manager.capture(job_id, settings.posthog_transcript_success)
return success_payload.model_dump()
@signals.task_prerun.connect(sender=process_audio_transcribe_v2_task)
def task_started(task_id=None, task=None, args=None, **kwargs):
def task_started_transcript(task_id=None, task=None, args=None, **kwargs):
"""Signal handler called before task execution begins."""
if args:
metadata_manager.create(task_id, TranscribeTaskJob.model_validate(args[0]))
@signals.task_retry.connect(sender=process_audio_transcribe_v2_task)
def task_retry_handler(request=None, reason=None, einfo=None, **kwargs):
def task_retry_handler_transcript(request=None, reason=None, einfo=None, **kwargs):
"""Signal handler called when task execution retries."""
metadata_manager.retry(request.id)
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
def task_failure_handler(task_id, exception=None, **kwargs):
def task_failure_handler_transcript(task_id, exception=None, **kwargs):
"""Signal handler called when task execution fails permanently."""
metadata_manager.capture(task_id, settings.posthog_event_failure)
metadata_manager.capture(task_id, settings.posthog_transcript_failure)
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
@@ -648,9 +648,30 @@ def summarize_v2_task(
call_webhook_v2_task.apply_async(
args=[success_payload.model_dump(), payload.tenant_id]
)
metadata_manager.capture(job_id, settings.posthog_summary_success)
return success_payload.model_dump()
@signals.task_prerun.connect(sender=summarize_v2_task)
def task_started_summary(task_id=None, task=None, args=None, **kwargs):
"""Signal handler called before task execution begins."""
if args:
metadata_manager.create(task_id, SummarizeTaskJob.model_validate(args[0]))
@signals.task_retry.connect(sender=summarize_v2_task)
def task_retry_handler_summary(request=None, reason=None, einfo=None, **kwargs):
"""Signal handler called when task execution retries."""
metadata_manager.retry(request.id)
@signals.task_failure.connect(sender=summarize_v2_task)
def task_failure_handler_summary(task_id, exception=None, **kwargs):
"""Signal handler called when task execution fails permanently."""
metadata_manager.capture(task_id, settings.posthog_summary_failure)
@signals.task_failure.connect(sender=summarize_v2_task)
def handle_summarize_v2_failed(
sender,
+6 -3
View File
@@ -130,9 +130,12 @@ class Settings(BaseSettings):
posthog_enabled: bool = False
posthog_api_key: Optional[str] = None
posthog_api_host: Optional[str] = "https://eu.i.posthog.com"
posthog_event_failure: str = "transcript-failure"
posthog_event_success: str = "transcript-success"
posthog_event_request: str = "transcript-request"
posthog_transcript_request: str = "transcript-request"
posthog_transcript_failure: str = "transcript-failure"
posthog_transcript_success: str = "transcript-success"
posthog_summary_request: str = "summary-request"
posthog_summary_failure: str = "summary-failure"
posthog_summary_success: str = "summary-success"
# Langfuse (LLM Observability)
langfuse_enabled: bool = False