Compare commits

...

21 Commits

Author SHA1 Message Date
lebaudantoine 1333e39164 🔧(frontend) enable dynacast optimizations
According to the documentation:

Dynacast dynamically pauses video layers that are
not being consumed by any subscribers, significantly
reducing publishing CPU and bandwidth usage.

Dynacast will be enabled if SVC codecs (VP9/AV1) are used.
Multi-codec simulcast requires dynacast

My goal is to reduce CPU and bandwidth usage for clients.

Dynacast is enabled both in OpenTalk and LiveKit demo app!
2024-12-09 17:34:12 +01:00
lebaudantoine 5931c5605a 🔧(frontend) enable adaptive stream optimizations
Adaptive stream is a key optimization in large room.

Enabled adaptive stream to automatically manage
the quality of subscribed video tracks, optimizing
for bandwidth and CPU usage.

When video elements are visible, it adjusts
the resolution based on the size of the largest visible
element. If no video elements are visible, it
temporarily pauses the track until they are visible again.

Additionally, introduced support for custom pixel density,
which defaults to `2` for high-density screens
(devicePixelRatio ≥ 3) or `1` for standard screens.
Setting it to `screen` allows the pixel density to match
the actual screen's devicePixelRatio, optimizing video
clarity on ultra-high-definition displays.

This ensures a balance between streaming quality
and resource consumption.

This might also significantly increase the bandwidth
consumed by people streaming on high definition screens.
It needs to be battle tested.

OpenTalk uses a adaptiveStream equals true, while LiveKit
demo app uses 'screen' value. I followed OpenTalk choice,
I was scared 'screen' value creates performance issues
for user with high resolution screen in poor network conditions.
2024-12-09 17:31:27 +01:00
lebaudantoine 3a205373dd 🔧(frontend) default to vp9 for video codec
As a mandatory codec in WebRTC specifications, VP8 serves as
the baseline for compatibility, making it the default choice
in LiveKit client configuration.

There is room for optimization.

Newer codecs like VP9 offer significant efficiency gains
compared to VP8, with a 23-33% improvement in compression
efficiency. This translates to better video quality at
the same bitrate or reduced bandwidth usage for the same quality.

VP9 is supported in Safari starting from version 15.0+,
and Firefox offers partial support. However, Firefox lacks
support for VP9's Scalable Video Coding (SVC).

With SVC, participants can send a single VP9 stream
with multiple spatial or temporal layers. This allows receivers
to dynamically adjust video quality by using lower layers when
resolution or bandwidth needs to be reduced, improving
adaptability in heterogeneous network conditions.

Simulcast, by contrast, sends multiple separate streams
at different resolutions. While widely supported in VP8 and VP9,
it consumes more bandwidth compared to SVC.

The configuration added here is based on the LiveKit demo app,
which defaults to VP9 when supported. OpenTalk’s configuration
also recommends VP9.

If a browser does not support VP9, LiveKit falls back to VP8 or
other codecs as needed. Notably, LiveKit disables VP9 encoding for
Firefox due to longstanding issues, but it can still decode VP9
streams and encode VP8 for outgoing streams. This ensures
compatibility with other participants, even in mixed environments
where some browsers use VP9 and others fallback to VP8.

In theory, participants do not all need to switch to a single codec,
as both LiveKit and browsers intelligently handle codec negotiation
on a per-participant basis. This dynamic adaptation ensures seamless
communication without manual intervention.

A similar challenge with codec compatibility was raised
in Jitsi two years ago, check issue #10657.

Before any release, this needs to be battle tested
with Firefox 115 browsers.
2024-12-09 17:20:07 +01:00
lebaudantoine 0b8181e5ce ✏️(backend) fix few typos
Fix few typos in the docstring.
2024-12-06 12:46:46 +01:00
lebaudantoine 8b2365d5f9 (backend) add ODC_VERIFY_SSL configuration
Need to disable SSL while authenticating to unsecure
OIDC provider in dev environment.
2024-12-06 12:46:46 +01:00
lebaudantoine 2dd16d1f40 🩹(backend) add missing environ_prefix on OIDC_CREATE_USER
Misleading, to not have all OIDC-related settings, with environ prefix at None.
Caught myself into troubleshooting few minutes.
2024-12-06 12:46:46 +01:00
lebaudantoine fa9484b630 (frontend) introduce a recording toaster
Notify visually users that the room is being recorded.
Draft, it will be enhance the future.
2024-12-04 18:38:26 +01:00
lebaudantoine 1e0e495cd8 ♻️(frontend) refactor pulse_mic into pulse_background
Make the keyframe more generic with an explicit naming.
2024-12-04 18:38:26 +01:00
lebaudantoine c270299179 🩹(frontend) fix submit button in feedbacks page
Since recent changes in the Color palette, the button was totally
invisible… fixed it.
2024-12-04 18:38:26 +01:00
lebaudantoine d9a84e5f0f 🔥(frontend) remove transcription menu item
This menu item was replaced by a side panel.
2024-12-04 18:38:26 +01:00
lebaudantoine 0f64d3cf3a (frontend) introduce a sidepanel for AI assistant
Introduce the content for the AI assistant panel, which describes the
feature, and offers a button to start and stop a recording.
2024-12-04 18:38:26 +01:00
lebaudantoine d1e008a844 ♻️(frontend) introduce useRoomId hook
Manipulating the room's id from the react-query cache should be
encapsulated in a dedicated hook.
2024-12-04 18:38:26 +01:00
lebaudantoine 3be5a5afc6 ♻️(frontend) package checks in useHasTranscriptAccess hook
This hook will be used by the toggle and the sidepanel to make
sure the users have sufficient permissions to access the transcript
features.
2024-12-04 18:38:26 +01:00
lebaudantoine 31468a5e7c ♻️(frontend) introduce a reusable isTranscriptEnabled
Encapsulate the logic, checking if the feature is enabled in the
backend, in a proper and reusable hook.
2024-12-04 18:38:26 +01:00
lebaudantoine b342b9d526 🚩(frontend) enable transcript toggle with a feature flag
Rely on Posthog for a first iteration on the feature flag feature.
This is a pragmatic choice, relying on an external dependency might
not suitable on the longer term, however, compare to the maturity
of our product, this is the best trade off.
2024-12-04 18:38:26 +01:00
lebaudantoine f7e7c3ba22 🚚(frontend) remove wrong 'tsx' extension
The hook only uses typescript code.
2024-12-04 18:38:26 +01:00
lebaudantoine 3902b02691 📈(frontend) check if analytic is enabled
Few frontend features rely on Posthog. Posthog is not activated in
dev environment. Offer a hook that encapsulates this logic, and
return a boolean flag.
2024-12-04 18:38:26 +01:00
lebaudantoine 7ce4390740 (frontend) initialize transcript sidebar panel
Setup base structure and styling for transcript menu sidebar
2024-12-04 18:38:26 +01:00
Jacques ROUSSEL 94d18cffe4 🔐(secrets) bump secretBump secret
In order to use our openai api we need new secret
2024-12-04 10:49:17 +01:00
lebaudantoine ad0c3eea66 🩹(summary) remove temporary audio files saved
I forgot to remove temporary files downloaded from the Minio bucket.
2024-12-04 10:49:17 +01:00
lebaudantoine 459bbf65a8 🔧(summary) use our self-deployed models
Configure dev and staging environment to use our self-deployed
models (Whisper and LLM). Secrets need to be updated btw.

Because of outscale LB bug, which timeout after 60s, we need to
connect directly to the svc.
2024-12-04 10:49:17 +01:00
29 changed files with 377 additions and 115 deletions
+1 -1
Submodule secrets updated: 142e7a70b1...f5aea0c251
+2 -2
View File
@@ -145,7 +145,7 @@ class OIDCLogoutCallbackView(MozillaOIDCOIDCLogoutView):
class OIDCAuthenticationCallbackView(MozillaOIDCAuthenticationCallbackView):
"""Custom callback view for handling the silent loging flow."""
"""Custom callback view for handling the silent login flow."""
@property
def failure_url(self):
@@ -162,7 +162,7 @@ class OIDCAuthenticationCallbackView(MozillaOIDCAuthenticationCallbackView):
class OIDCAuthenticationRequestView(MozillaOIDCAuthenticationRequestView):
"""Custom authentication view for handling the silent loging flow."""
"""Custom authentication view for handling the silent login flow."""
def get_extra_params(self, request):
"""Handle 'prompt' extra parameter for the silent login flow
+4 -2
View File
@@ -324,8 +324,10 @@ class Base(Configuration):
OIDC_AUTHENTICATE_CLASS = "core.authentication.views.OIDCAuthenticationRequestView"
OIDC_CALLBACK_CLASS = "core.authentication.views.OIDCAuthenticationCallbackView"
OIDC_CREATE_USER = values.BooleanValue(
default=True,
environ_name="OIDC_CREATE_USER",
default=True, environ_name="OIDC_CREATE_USER", environ_prefix=None
)
OIDC_VERIFY_SSL = values.BooleanValue(
default=True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None
)
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue(
default=False,
+4 -4
View File
@@ -116,10 +116,10 @@ const config: Config = {
'80%': { transform: 'rotate(20deg)' },
'100%': { transform: 'rotate(0)' },
},
pulse_mic: {
'0%': { color: 'primary', opacity: '1' },
'50%': { color: 'primary', opacity: '0.8' },
'100%': { color: 'primary', opacity: '1' },
pulse_background: {
'0%': { opacity: '1' },
'50%': { opacity: '0.65' },
'100%': { opacity: '1' },
},
},
tokens: defineTokens({
@@ -0,0 +1,6 @@
import { useConfig } from '@/api/useConfig.ts'
export const useIsAnalyticsEnabled = () => {
const { data } = useConfig()
return !!data?.analytics?.id
}
@@ -65,6 +65,11 @@ export const Conference = ({
const roomOptions = useMemo((): RoomOptions => {
return {
adaptiveStream: true,
dynacast: true,
publishDefaults: {
videoCodec: 'vp9',
},
videoCaptureDefaults: {
deviceId: userConfig.videoDeviceId ?? undefined,
},
@@ -121,7 +121,13 @@ const OpenFeedback = ({
>
{t('submit')}
</Button>
<Button invisible size="sm" fullWidth onPress={onNext}>
<Button
invisible
variant="secondary"
size="sm"
fullWidth
onPress={onNext}
>
{t('skip')}
</Button>
</VStack>
@@ -0,0 +1,43 @@
import { css } from '@/styled-system/css'
import { RiRecordCircleLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useRoomContext } from '@livekit/components-react'
export const RecordingStateToast = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'recording' })
const room = useRoomContext()
if (!room?.isRecording) return
return (
<div
className={css({
display: 'flex',
position: 'fixed',
top: '10px',
left: '10px',
paddingY: '0.25rem',
paddingX: '0.25rem 0.35rem',
backgroundColor: 'primaryDark.200',
borderColor: 'primaryDark.400',
border: '1px solid',
color: 'white',
borderRadius: '4px',
gap: '0.5rem',
})}
>
<RiRecordCircleLine
size={20}
className={css({
color: 'white',
backgroundColor: 'danger.700',
padding: '3px',
borderRadius: '3px',
})}
/>
<Text variant={'sm'}>{t('label')}</Text>
</div>
)
}
@@ -10,6 +10,7 @@ import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { Effects } from './Effects'
import { Chat } from '../prefabs/Chat'
import { Transcript } from './Transcript'
type StyledSidePanelProps = {
title: string
@@ -106,6 +107,7 @@ export const SidePanel = () => {
isEffectsOpen,
isChatOpen,
isSidePanelOpen,
isTranscriptOpen,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
@@ -127,6 +129,9 @@ export const SidePanel = () => {
<Panel isOpen={isChatOpen}>
<Chat />
</Panel>
<Panel isOpen={isTranscriptOpen}>
<Transcript />
</Panel>
</StyledSidePanel>
)
}
@@ -0,0 +1,100 @@
import { Button, Div, H, Text } from '@/primitives'
import thirdSlide from '@/assets/intro-slider/3_resume.png'
import { css } from '@/styled-system/css'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import { RiRecordCircleLine, RiStopCircleLine } from '@remixicon/react'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
export const Transcript = () => {
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
const hasTranscriptAccess = useHasTranscriptAccess()
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const room = useRoomContext()
useEffect(() => {
const handleRecordingStatusChanged = () => {
setIsLoading(false)
}
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
return () => {
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
}
}, [room])
const handleTranscript = async () => {
if (!roomId) {
console.warn('No room ID found')
return
}
try {
setIsLoading(true)
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
}
} catch (error) {
console.error('Failed to handle transcript:', error)
setIsLoading(false)
}
}
if (!hasTranscriptAccess) return
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
flexDirection="column"
alignItems="center"
>
<img src={thirdSlide} alt={'wip'} />
{room.isRecording ? (
<>
<H lvl={2}>{t('stop.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('stop.body')}
</Text>
<div className={css({ height: '2rem' })} />
<Button isDisabled={isLoading} onPress={() => handleTranscript()}>
<RiStopCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('stop.button')}
</Button>
</>
) : (
<>
<H lvl={2}>{t('start.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('start.body')}
</Text>
<div className={css({ height: '2rem' })} />
<Button isDisabled={isLoading} onPress={() => handleTranscript()}>
<RiRecordCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('start.button')}
</Button>
</>
)}
</Div>
)
}
@@ -10,7 +10,6 @@ import { DialogState } from './OptionsButton'
import { Separator } from '@/primitives/Separator'
import { useSidePanel } from '../../../hooks/useSidePanel'
import { menuRecipe } from '@/primitives/menuRecipe.ts'
import { TranscriptMenuItem } from './TranscriptMenuItem'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = ({
@@ -35,7 +34,6 @@ export const OptionsMenuItems = ({
<RiAccountBoxLine size={20} />
{t('effects')}
</MenuItem>
<TranscriptMenuItem />
</Section>
<Separator />
<Section>
@@ -1,71 +0,0 @@
import { RiRecordCircleLine, RiStopCircleLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { MenuItem } from 'react-aria-components'
import {
RecordingMode,
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useRoomContext } from '@livekit/components-react'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { useConfig } from '@/api/useConfig'
export const TranscriptMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const apiRoomData = useRoomData()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const { data } = useConfig()
const room = useRoomContext()
const handleTranscript = async () => {
const roomId = apiRoomData?.livekit?.room
if (!roomId) {
console.warn('No room ID found')
return
}
try {
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
}
} catch (error) {
console.error('Failed to handle transcript:', error)
}
}
if (
!data?.recording?.is_enabled ||
!data?.recording?.available_modes?.includes(RecordingMode.Transcript) ||
!apiRoomData?.is_administrable
) {
return
}
return (
<MenuItem
className={menuRecipe({ icon: true }).item}
onAction={async () => await handleTranscript()}
>
{room.isRecording ? (
<>
<RiRecordCircleLine size={20} />
{t('transcript.stop')}
</>
) : (
<>
<RiStopCircleLine size={20} />
{t('transcript.start')}
</>
)}
</MenuItem>
)
}
@@ -84,9 +84,12 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
<RiMicOffFill color={'gray'} />
) : (
<RiMicFill
style={{
animation: isSpeaking ? 'pulse_mic 800ms infinite' : undefined,
}}
className={css({
color: isSpeaking ? 'primaryDark.300' : 'primaryDark.50',
animation: isSpeaking
? 'pulse_background 800ms infinite'
: undefined,
})}
/>
)}
</Button>
@@ -0,0 +1,37 @@
import { ToggleButton } from '@/primitives'
import { RiBardLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useSidePanel } from '../../hooks/useSidePanel'
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
import { css } from '@/styled-system/css'
export const TranscriptToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
const tooltipLabel = isTranscriptOpen ? 'open' : 'closed'
const hasTranscriptAccess = useHasTranscriptAccess()
if (!hasTranscriptAccess) return
return (
<div
className={css({
position: 'relative',
display: 'inline-block',
})}
>
<ToggleButton
square
variant="primaryTextDark"
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isTranscriptOpen}
onPress={() => toggleTranscript()}
>
<RiBardLine />
</ToggleButton>
</div>
)
}
@@ -0,0 +1,17 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { useIsTranscriptEnabled } from './useIsTranscriptEnabled'
import { useIsAdminOrOwner } from './useIsAdminOrOwner'
export const useHasTranscriptAccess = () => {
const featureEnabled = useFeatureFlagEnabled('transcription-summary')
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isTranscriptEnabled = useIsTranscriptEnabled()
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
isAdminOrOwner &&
isTranscriptEnabled
)
}
@@ -0,0 +1,6 @@
import { useRoomData } from './useRoomData'
export const useIsAdminOrOwner = () => {
const apiRoomData = useRoomData()
return apiRoomData?.is_administrable
}
@@ -0,0 +1,11 @@
import { RecordingMode } from '@/features/rooms/api/startRecording'
import { useConfig } from '@/api/useConfig'
export const useIsTranscriptEnabled = () => {
const { data } = useConfig()
return (
data?.recording?.is_enabled &&
data?.recording?.available_modes?.includes(RecordingMode.Transcript)
)
}
@@ -0,0 +1,6 @@
import { useRoomData } from './useRoomData'
export const useRoomId = () => {
const apiRoomData = useRoomData()
return apiRoomData?.livekit?.room
}
@@ -5,6 +5,7 @@ export enum PanelId {
PARTICIPANTS = 'participants',
EFFECTS = 'effects',
CHAT = 'chat',
TRANSCRIPT = 'transcript',
}
export const useSidePanel = () => {
@@ -14,6 +15,7 @@ export const useSidePanel = () => {
const isParticipantsOpen = activePanelId == PanelId.PARTICIPANTS
const isEffectsOpen = activePanelId == PanelId.EFFECTS
const isChatOpen = activePanelId == PanelId.CHAT
const isTranscriptOpen = activePanelId == PanelId.TRANSCRIPT
const isSidePanelOpen = !!activePanelId
const toggleParticipants = () => {
@@ -28,14 +30,20 @@ export const useSidePanel = () => {
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
}
const toggleTranscript = () => {
layoutStore.activePanelId = isTranscriptOpen ? null : PanelId.TRANSCRIPT
}
return {
activePanelId,
toggleParticipants,
toggleChat,
toggleEffects,
toggleTranscript,
isChatOpen,
isParticipantsOpen,
isEffectsOpen,
isSidePanelOpen,
isTranscriptOpen,
}
}
@@ -13,8 +13,10 @@ import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
import { SupportToggle } from '../components/controls/SupportToggle'
import { TranscriptToggle } from '../components/controls/TranscriptToggle'
import { css } from '@/styled-system/css'
import { SupportToggle } from '@/features/rooms/livekit/components/controls/SupportToggle.tsx'
/** @public */
export type ControlBarControls = {
@@ -156,6 +158,7 @@ export function ControlBar({
>
<ChatToggle />
<ParticipantsToggle />
<TranscriptToggle />
<SupportToggle />
</div>
</div>
@@ -28,6 +28,7 @@ import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingStateToast } from '../components/RecordingStateToast'
const LayoutWrapper = styled(
'div',
@@ -212,6 +213,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
)}
<RoomAudioRenderer />
<ConnectionStateToast />
<RecordingStateToast />
</div>
)
}
+24 -7
View File
@@ -71,6 +71,10 @@
"open": "",
"closed": ""
},
"transcript": {
"open": "",
"closed": ""
},
"support": ""
},
"options": {
@@ -79,11 +83,7 @@
"feedbacks": "",
"settings": "",
"username": "",
"effects": "",
"transcript": {
"start": "",
"stop": ""
}
"effects": ""
}
},
"effects": {
@@ -102,18 +102,32 @@
"heading": {
"participants": "",
"effects": "",
"chat": ""
"chat": "",
"transcript": ""
},
"content": {
"participants": "",
"effects": "",
"chat": ""
"chat": "",
"transcript": ""
},
"closeButton": ""
},
"chat": {
"disclaimer": ""
},
"transcript": {
"start": {
"heading": "",
"body": "",
"button": ""
},
"stop": {
"heading": "",
"body": "",
"button": ""
}
},
"rating": {
"submit": "",
"question": "",
@@ -150,5 +164,8 @@
"raisedHands": "",
"lowerParticipantHand": "",
"lowerParticipantsHand": ""
},
"recording": {
"label": ""
}
}
+24 -7
View File
@@ -70,6 +70,10 @@
"open": "Hide everyone",
"closed": "See everyone"
},
"transcript": {
"open": "Hide AI assistant",
"closed": "Show AI assistant"
},
"support": "Support"
},
"options": {
@@ -78,11 +82,7 @@
"feedbacks": "Give us feedbacks",
"settings": "Settings",
"username": "Update Your Name",
"effects": "Apply effects",
"transcript": {
"start": "Start meeting transcription",
"stop": "Stop ongoing transcription"
}
"effects": "Apply effects"
}
},
"effects": {
@@ -101,18 +101,32 @@
"heading": {
"participants": "Participants",
"effects": "Effects",
"chat": "Messages in the chat"
"chat": "Messages in the chat",
"transcript": "AI Assistant"
},
"content": {
"participants": "participants",
"effects": "effects",
"chat": "messages"
"chat": "messages",
"transcript": "AI assistant"
},
"closeButton": "Hide {{content}}"
},
"chat": {
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
},
"transcript": {
"start": {
"heading": "Start the Assistant!",
"body": "The assistant automatically starts recording your meeting audio (limited to 1 hour). At the end, you'll receive a clear and concise summary of the discussion directly via email.",
"button": "Start"
},
"stop": {
"heading": "Recording in Progress...",
"body": "Your meeting is currently being recorded. You will receive a summary via email once the meeting ends.",
"button": "Stop Recording"
}
},
"rating": {
"submit": "Submit",
"question": "What do you think about the quality of your call?",
@@ -149,5 +163,8 @@
"raisedHands": "Raised hands",
"lowerParticipantHand": "Lower {{name}}'s hand",
"lowerParticipantsHand": "Lower all hands"
},
"recording": {
"label": "Recording"
}
}
+24 -7
View File
@@ -70,6 +70,10 @@
"open": "Masquer les participants",
"closed": "Afficher les participants"
},
"transcript": {
"open": "Masquer l'assistant IA",
"closed": "Afficher l'assistant IA"
},
"support": "Support"
},
"options": {
@@ -78,11 +82,7 @@
"feedbacks": "Partager votre avis",
"settings": "Paramètres",
"username": "Choisir votre nom",
"effects": "Appliquer des effets",
"transcript": {
"start": "Démarrer la transcription",
"stop": "Arrêter la transcription en cours"
}
"effects": "Appliquer des effets"
}
},
"effects": {
@@ -101,18 +101,32 @@
"heading": {
"participants": "Participants",
"effects": "Effets",
"chat": "Messages dans l'appel"
"chat": "Messages dans l'appel",
"transcript": "Assistant IA"
},
"content": {
"participants": "les participants",
"effects": "les effets",
"chat": "les messages"
"chat": "les messages",
"transcript": "l'assistant IA"
},
"closeButton": "Masquer {{content}}"
},
"chat": {
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
},
"transcript": {
"start": {
"heading": "Démarrer l'assistant !",
"body": "L'assistant démarre automatiquement l'enregistrement sonore de votre réunion (limité à 1h). À la fin, vous recevrez un résumé clair et concis des échanges directement par e-mail.",
"button": "Démarrer"
},
"stop": {
"heading": "Enregistrement en cours …",
"body": "L'enregistrement de votre réunion est en cours. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Arrêter l'enregistrement"
}
},
"rating": {
"submit": "Envoyer",
"question": "Que pensez-vous de la qualité de votre appel ?",
@@ -149,5 +163,8 @@
"raisedHands": "Mains levées",
"lowerParticipantHand": "Baisser la main de {{name}}",
"lowerParticipantsHand": "Baisser la main de tous les participants"
},
"recording": {
"label": "Enregistrement"
}
}
+8
View File
@@ -55,6 +55,14 @@ export const text = cva({
textAlign: 'inherit',
},
},
wrap: {
balance: {
textWrap: 'balance',
},
pretty: {
textWrap: 'pretty',
},
},
bold: {
true: {
fontWeight: 'bold',
@@ -125,6 +125,9 @@ summary:
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
@@ -155,6 +158,9 @@ celery:
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
@@ -216,6 +216,9 @@ summary:
secretKeyRef:
name: summary
key: OPENAI_API_KEY
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN:
secretKeyRef:
name: summary
+14 -7
View File
@@ -1,6 +1,7 @@
"""Celery workers."""
import json
import os
import tempfile
from pathlib import Path
@@ -28,6 +29,7 @@ celery = Celery(
)
if settings.sentry_dsn and settings.sentry_is_enabled:
@signals.celeryd_init.connect
def init_sentry(**_kwargs):
"""Initialize sentry."""
@@ -100,15 +102,20 @@ def process_audio_transcribe_summarize(filename: str, email: str, sub: str):
api_key=settings.openai_api_key, base_url=settings.openai_base_url
)
logger.debug("Querying transcription …")
with open(temp_file_path, "rb") as audio_file:
transcription = openai_client.audio.transcriptions.create(
model=settings.openai_asr_model, file=audio_file
)
try:
logger.debug("Querying transcription …")
with open(temp_file_path, "rb") as audio_file:
transcription = openai_client.audio.transcriptions.create(
model=settings.openai_asr_model, file=audio_file
)
transcription = transcription.text
transcription = transcription.text
logger.debug("Transcription: \n %s", transcription)
logger.debug("Transcription: \n %s", transcription)
finally:
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
logger.debug("Temporary file removed: %s", temp_file_path)
instructions = get_instructions(transcription)
summary_response = openai_client.chat.completions.create(