mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 04:09:26 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 898bc9a0f8 | |||
| fd1715bacf | |||
| c9de7d049f | |||
| 6368b676a6 | |||
| 65789ef706 | |||
| c0feb1ee82 | |||
| 135b99aee7 | |||
| 040df0e15a | |||
| 00e197b216 | |||
| eae1a382d5 | |||
| d4a7cf279c | |||
| 70a296eea6 | |||
| ac85a20271 | |||
| 13036f6ab7 |
+23
-1
@@ -8,6 +8,28 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.21.0] - 2026-06-15
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) allow disabling silent login via a URL parameter
|
||||
- ✨(frontend) allow hiding the login button via a URL parameter
|
||||
- ✨(summary) add optional satisfaction survey footer
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(frontend) enhance noise reduction with BBBA audio processing pipeline
|
||||
- 🚸(frontend) mute join notification sound in larger rooms
|
||||
- 🚸(frontend) mute participants by default when joining a large meeting
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix metadata agent collector enabled check
|
||||
|
||||
### Fixed
|
||||
|
||||
- ♿️(frontend) improve accessibilty of the Effects panel #1401
|
||||
|
||||
## [1.20.0] - 2026-06-12
|
||||
|
||||
### Changed
|
||||
@@ -36,7 +58,7 @@ and this project adheres to
|
||||
|
||||
- 🔇(summary) make ffmpeg quiet #1404
|
||||
- 🔒️(backend) prevent accessing files if they are not ready #1395
|
||||
- ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
|
||||
- # ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
|
||||
|
||||
## [1.18.0] - 2026-06-03
|
||||
|
||||
|
||||
@@ -22,3 +22,6 @@ WEBHOOK_URL="https://configure-your-url.com"
|
||||
|
||||
POSTHOG_API_KEY="your-posthog-key"
|
||||
POSTHOG_ENABLED="False"
|
||||
|
||||
# Transcription
|
||||
TRANSCRIPTION_SATISFACTION_FORM_BASE_URL=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.5.13",
|
||||
|
||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
|
||||
[[package]]
|
||||
name = "agents"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "livekit-agents" },
|
||||
|
||||
@@ -411,6 +411,12 @@ class Base(Configuration):
|
||||
"transcription_destination": values.Value(
|
||||
None, environ_name="FRONTEND_TRANSCRIPTION_DESTINATION", environ_prefix=None
|
||||
),
|
||||
"max_participants_for_sound": values.PositiveIntegerValue(
|
||||
5, environ_name="FRONTEND_MAX_PARTICIPANTS_FOR_SOUND", environ_prefix=None
|
||||
),
|
||||
"auto_mute_on_join_threshold": values.PositiveIntegerValue(
|
||||
50, environ_name="FRONTEND_AUTO_MUTE_ON_JOIN_THRESHOLD", environ_prefix=None
|
||||
),
|
||||
}
|
||||
|
||||
# Mail
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Generated
+9
-2
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@libreaudio/la-call": "0.1.4",
|
||||
"@livekit/components-react": "2.9.21",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
"@livekit/track-processors": "0.7.2",
|
||||
@@ -1446,6 +1447,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@libreaudio/la-call": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@libreaudio/la-call/-/la-call-0.1.4.tgz",
|
||||
"integrity": "sha512-yaqlYGpvZbfpr1h2p1jSGOKPUy7NsfpoJN9gXvHmJu5MB8nN/qfx6LQ2a0hbua3H8rargFKROOv4VZ5WuTKNsA==",
|
||||
"license": "GPL-3.0+"
|
||||
},
|
||||
"node_modules/@livekit/components-core": {
|
||||
"version": "0.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@libreaudio/la-call": "0.1.4",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@livekit/components-react": "2.9.21",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
|
||||
@@ -55,6 +55,8 @@ export interface ApiConfig {
|
||||
default_sources: Source[]
|
||||
}
|
||||
transcription_destination?: string
|
||||
max_participants_for_sound: number
|
||||
auto_mute_on_join_threshold: number
|
||||
}
|
||||
|
||||
const fetchConfig = (): Promise<ApiConfig> => {
|
||||
|
||||
@@ -5,6 +5,16 @@ import { type ApiUser } from './ApiUser'
|
||||
import { useMemo } from 'react'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
const SILENT_LOGIN_PARAM = 'silentLogin'
|
||||
|
||||
const isSilentLoginDisabledByUrl = () => {
|
||||
if (typeof window === 'undefined') return false
|
||||
const value = new URLSearchParams(window.location.search).get(
|
||||
SILENT_LOGIN_PARAM
|
||||
)
|
||||
return value === 'false'
|
||||
}
|
||||
|
||||
/**
|
||||
* returns info about currently logged-in user
|
||||
*
|
||||
@@ -17,16 +27,22 @@ export const useUser = (
|
||||
) => {
|
||||
const { data, isLoading: isConfigLoading } = useConfig()
|
||||
|
||||
const disabledByUrl = useMemo(() => isSilentLoginDisabledByUrl(), [])
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (isConfigLoading) return
|
||||
if (data?.is_silent_login_enabled !== true) {
|
||||
|
||||
const silentDisabled =
|
||||
data?.is_silent_login_enabled !== true || disabledByUrl
|
||||
|
||||
if (silentDisabled) {
|
||||
return {
|
||||
...opts,
|
||||
...opts.fetchUserOptions,
|
||||
attemptSilent: false,
|
||||
}
|
||||
}
|
||||
return opts.fetchUserOptions
|
||||
}, [data, opts, isConfigLoading])
|
||||
}, [data, opts, isConfigLoading, disabledByUrl])
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [keys.user],
|
||||
|
||||
@@ -14,9 +14,11 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { Emoji } from '@/features/reactions/types'
|
||||
import { useReactions } from '@/features/reactions/hooks/useReactions'
|
||||
import { NotificationProvider } from './NotificationProvider'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
export const MainNotificationToast = () => {
|
||||
const room = useRoomContext()
|
||||
const { data } = useConfig()
|
||||
const { triggerNotificationSound } = useNotificationSound()
|
||||
const { t } = useTranslation('notifications')
|
||||
const announce = useScreenReaderAnnounce()
|
||||
@@ -135,12 +137,21 @@ export const MainNotificationToast = () => {
|
||||
}
|
||||
}, [room, handleEmoji])
|
||||
|
||||
const triggerNotificationSoundIfRoomIsSmall = useCallback(
|
||||
(type: NotificationType) => {
|
||||
if (!data) return
|
||||
if (room.numParticipants >= data.max_participants_for_sound) return
|
||||
triggerNotificationSound(type)
|
||||
},
|
||||
[room, data, triggerNotificationSound]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const showJoinNotification = (participant: Participant) => {
|
||||
if (isMobileBrowser()) {
|
||||
return
|
||||
}
|
||||
triggerNotificationSound(NotificationType.ParticipantJoined)
|
||||
triggerNotificationSoundIfRoomIsSmall(NotificationType.ParticipantJoined)
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
@@ -155,7 +166,7 @@ export const MainNotificationToast = () => {
|
||||
return () => {
|
||||
room.off(RoomEvent.ParticipantConnected, showJoinNotification)
|
||||
}
|
||||
}, [room, triggerNotificationSound])
|
||||
}, [room, triggerNotificationSoundIfRoomIsSmall])
|
||||
|
||||
useEffect(() => {
|
||||
const removeParticipantNotifications = (participant: Participant) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NotificationType {
|
||||
AutoMuteLargeRoom = 'autoMuteLargeRoom',
|
||||
ParticipantJoined = 'participantJoined',
|
||||
HandRaised = 'handRaised',
|
||||
ParticipantMuted = 'participantMuted',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useToast } from '@react-aria/toast'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import { type ToastProps } from './Toast'
|
||||
import { VStack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { StyledToastContainer } from './StyledToastContainer'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
|
||||
export function ToastAutoMuteLargeRoom({
|
||||
state,
|
||||
...props
|
||||
}: Readonly<ToastProps>) {
|
||||
const room = useRoomContext()
|
||||
const { t } = useTranslation('notifications', {
|
||||
keyPrefix: 'autoMuteLargeRoom',
|
||||
})
|
||||
const ref = useRef(null)
|
||||
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||
const toast = props.toast
|
||||
|
||||
const handleDismiss = async () => {
|
||||
room.localParticipant
|
||||
.setMicrophoneEnabled(true)
|
||||
.finally(() => state.close(toast.key))
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<VStack
|
||||
justify="start"
|
||||
alignItems="self-start"
|
||||
{...contentProps}
|
||||
maxWidth="370px"
|
||||
gap="0.75rem"
|
||||
padding={14}
|
||||
>
|
||||
<p>{t('auto')}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="text"
|
||||
className={css({
|
||||
color: 'primary.300',
|
||||
})}
|
||||
onPress={() => handleDismiss()}
|
||||
>
|
||||
{t('dismiss')}
|
||||
</Button>
|
||||
</VStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { ToastAnyRecording } from './ToastAnyRecording'
|
||||
import { ToastRecordingSaving } from './ToastRecordingSaving'
|
||||
import { ToastPermissionsRemoved } from './ToastPermissionsRemoved'
|
||||
import { ToastRecordingRequest } from './ToastRecordingRequest'
|
||||
import { ToastAutoMuteLargeRoom } from './ToastAutoMuteLargeRoom'
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<ToastData>
|
||||
@@ -45,6 +46,11 @@ const renderToast = (
|
||||
case NotificationType.LowerHand:
|
||||
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
|
||||
|
||||
case NotificationType.AutoMuteLargeRoom:
|
||||
return (
|
||||
<ToastAutoMuteLargeRoom key={toast.key} toast={toast} state={state} />
|
||||
)
|
||||
|
||||
case NotificationType.TranscriptionStarted:
|
||||
case NotificationType.TranscriptionStopped:
|
||||
case NotificationType.TranscriptionLimitReached:
|
||||
|
||||
@@ -5,6 +5,15 @@ import type { Participant } from 'livekit-client'
|
||||
import type { NotificationPayload } from './NotificationPayload'
|
||||
import type { RecordingMode } from '@/features/recording'
|
||||
|
||||
export const notifyAutoMutedOnJoin = () => {
|
||||
toastQueue.add(
|
||||
{
|
||||
type: NotificationType.AutoMuteLargeRoom,
|
||||
},
|
||||
{ timeout: NotificationDuration.ALERT }
|
||||
)
|
||||
}
|
||||
|
||||
export const showLowerHandToast = (
|
||||
participant: Participant,
|
||||
onClose: () => void
|
||||
|
||||
@@ -6,5 +6,5 @@ export const useIsMetadataCollectorEnabled = () => {
|
||||
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.metadataCollector)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
return (featureEnabled && isAnalyticsEnabled) || true
|
||||
return !isAnalyticsEnabled || Boolean(featureEnabled)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@@ -33,6 +33,9 @@ import { useIsMobile } from '@/utils/useIsMobile'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
|
||||
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -57,6 +60,8 @@ export const Conference = ({
|
||||
|
||||
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
|
||||
|
||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||
|
||||
const {
|
||||
mutateAsync: createRoom,
|
||||
status: createStatus,
|
||||
@@ -173,6 +178,8 @@ export const Conference = ({
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const hasAutoMutedRef = useRef(false)
|
||||
|
||||
/*
|
||||
* Ensure stable WebSocket connection URL. This is critical for legacy browser compatibility
|
||||
* (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs in WebSocket() constructor
|
||||
@@ -228,6 +235,19 @@ export const Conference = ({
|
||||
onError={(e) => {
|
||||
posthog.captureException(e)
|
||||
}}
|
||||
onConnected={async () => {
|
||||
if (!apiConfig) return
|
||||
if (
|
||||
userPreferencesSnap.is_auto_mute_large_room_enabled &&
|
||||
!hasAutoMutedRef.current &&
|
||||
userConfig.audioEnabled &&
|
||||
room.numParticipants > apiConfig.auto_mute_on_join_threshold
|
||||
) {
|
||||
hasAutoMutedRef.current = true
|
||||
await room.localParticipant.setMicrophoneEnabled(false)
|
||||
notifyAutoMutedOnJoin()
|
||||
}
|
||||
}}
|
||||
onDisconnected={(e) => {
|
||||
const metadata = {
|
||||
room_id: roomId,
|
||||
|
||||
@@ -216,6 +216,14 @@ export const Join = ({
|
||||
try {
|
||||
const track = await createLocalAudioTrack({
|
||||
deviceId: { exact: audioDeviceId },
|
||||
noiseSuppression: true,
|
||||
echoCancellation: true,
|
||||
autoGainControl: true,
|
||||
voiceIsolation: false,
|
||||
// Audio quality optimized for voice
|
||||
sampleRate: 48000, // High quality sample rate
|
||||
channelCount: 1, // Mono for voice calls (saves bandwidth)
|
||||
sampleSize: 16, // 16-bit audio
|
||||
})
|
||||
setDynamicAudioTrack(track)
|
||||
} catch (error) {
|
||||
|
||||
+95
-65
@@ -400,6 +400,8 @@ export const EffectsConfiguration = ({
|
||||
config: ProcessorConfig
|
||||
isSelected: boolean
|
||||
tooltip: string
|
||||
ariaLabel: string
|
||||
ariaDeleteLabel: string
|
||||
file: ApiFileItem
|
||||
}[]
|
||||
}>(() => {
|
||||
@@ -426,7 +428,9 @@ export const EffectsConfiguration = ({
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
return {
|
||||
id,
|
||||
tooltip: t(`blur.light.${selectedId === id ? 'clear' : 'apply'}`),
|
||||
tooltip: t(
|
||||
`blur.${item.key}.${selectedId === id ? 'clear' : 'apply'}`
|
||||
),
|
||||
radius: item.radius,
|
||||
isSelected: selectedId === id,
|
||||
Icon: item.icon,
|
||||
@@ -443,9 +447,11 @@ export const EffectsConfiguration = ({
|
||||
}
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
const isSelected = selectedId === id
|
||||
const prefix = isSelected ? 'selectedLabel' : 'apply'
|
||||
const backgroundName = t(`virtual.presets.descriptions.${index}`)
|
||||
const ariaLabel = `${t(`virtual.presets.${prefix}`)} ${backgroundName}`
|
||||
const ariaLabelPrefix = t(
|
||||
isSelected ? `virtual.selectedLabel` : `virtual.apply`
|
||||
)
|
||||
const ariaLabel = `${ariaLabelPrefix} ${backgroundName}`
|
||||
|
||||
return {
|
||||
tooltip: backgroundName,
|
||||
@@ -467,13 +473,22 @@ export const EffectsConfiguration = ({
|
||||
}
|
||||
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
const isSelected = selectedId === id
|
||||
const ariaLabel = t(
|
||||
isSelected
|
||||
? `virtual.personal.selectedLabel`
|
||||
: `virtual.personal.apply`
|
||||
)
|
||||
const ariaDeleteLabel = t('virtual.personal.deleteLabel')
|
||||
|
||||
return {
|
||||
tooltip: file.title,
|
||||
id,
|
||||
config,
|
||||
isSelected: selectedId === id,
|
||||
isSelected,
|
||||
file,
|
||||
ariaLabel,
|
||||
ariaDeleteLabel,
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -552,6 +567,7 @@ export const EffectsConfiguration = ({
|
||||
[layout === 'vertical' ? 'height' : 'minHeight']: '175px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@@ -664,7 +680,7 @@ export const EffectsConfiguration = ({
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={2}
|
||||
lvl={3}
|
||||
style={{
|
||||
marginBottom: '0.4rem',
|
||||
}}
|
||||
@@ -680,6 +696,7 @@ export const EffectsConfiguration = ({
|
||||
paddingBottom: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
role="list"
|
||||
>
|
||||
{createFileMutation.isPending &&
|
||||
fileBeingUploadedObjectUrlRef.current && (
|
||||
@@ -721,48 +738,54 @@ export const EffectsConfiguration = ({
|
||||
className={
|
||||
'hoverGroup ' + css({ position: 'relative' })
|
||||
}
|
||||
role="listitem"
|
||||
>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.tooltip}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={getHandleSelectChangeFile(option.file)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.file.url!})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${option.file.id}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<Button
|
||||
className={
|
||||
'hoverGroupChild ' +
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
})
|
||||
}
|
||||
size={'xs'}
|
||||
variant={'tertiary'}
|
||||
onClick={() => {
|
||||
if (option.isSelected) {
|
||||
// we remove the current effect
|
||||
toggleEffect(option.config)
|
||||
<div role="group" aria-label={option.file.title}>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={getHandleSelectChangeFile(
|
||||
option.file
|
||||
)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.file.url!})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${option.file.id}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<Button
|
||||
className={
|
||||
'hoverGroupChild ' +
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
})
|
||||
}
|
||||
deleteFileMutation.mutate({
|
||||
fileId: option.file.id,
|
||||
})
|
||||
}}
|
||||
isDisabled={deleteFileMutation.isPending}
|
||||
>
|
||||
<RiDeleteBinLine size={16} />
|
||||
</Button>
|
||||
aria-label={option.ariaDeleteLabel}
|
||||
size={'xs'}
|
||||
variant={'tertiary'}
|
||||
onClick={() => {
|
||||
if (option.isSelected) {
|
||||
// we remove the current effect
|
||||
toggleEffect(option.config)
|
||||
}
|
||||
deleteFileMutation.mutate({
|
||||
fileId: option.file.id,
|
||||
})
|
||||
}}
|
||||
isDisabled={deleteFileMutation.isPending}
|
||||
>
|
||||
<RiDeleteBinLine size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
@@ -775,9 +798,13 @@ export const EffectsConfiguration = ({
|
||||
>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={
|
||||
uploadNotPossibleSnap.imageBackgroundConfig.label
|
||||
}
|
||||
aria-label={`${t(
|
||||
deriveIdFromProcessorConfig(
|
||||
uploadNotPossibleSnap.imageBackgroundConfig
|
||||
) === selectedId
|
||||
? `virtual.selectedLabel`
|
||||
: `virtual.apply`
|
||||
)} ${uploadNotPossibleSnap.imageBackgroundConfig.label}`}
|
||||
isDisabled={
|
||||
processorOptions.isDisabled ||
|
||||
createFileMutation.isPending
|
||||
@@ -857,7 +884,7 @@ export const EffectsConfiguration = ({
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={2}
|
||||
lvl={3}
|
||||
style={{
|
||||
marginBottom: '0.4rem',
|
||||
}}
|
||||
@@ -872,24 +899,27 @@ export const EffectsConfiguration = ({
|
||||
paddingBottom: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
role="list"
|
||||
>
|
||||
{processorOptions.virtualBackgrounds.map((option) => (
|
||||
<VisualOnlyTooltip key={option.id} tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => toggleEffect(option.config)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-preset-${option.index}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<div role="listitem" key={option.id}>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => toggleEffect(option.config)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-preset-${option.index}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import type { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
|
||||
import { NoiseSuppressorWorklet_Name } from '@timephy/rnnoise-wasm'
|
||||
|
||||
// This is an example how to get the script path using Vite, may be different when using other build tools
|
||||
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
|
||||
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
|
||||
|
||||
// Use Jitsi's approach: maintain a global AudioContext variable
|
||||
// and suspend/resume it as needed to manage audio state
|
||||
let audioContext: AudioContext
|
||||
|
||||
/**
|
||||
* Lazily load the WASM processor factory.
|
||||
*
|
||||
* '@libreaudio/la-call' pulls in a WebAssembly payload, so we defer importing
|
||||
* it until a processor is actually initialized rather than at module load.
|
||||
* The import promise is cached so repeated init() calls reuse the same module.
|
||||
*/
|
||||
type CreateWasmProcessor =
|
||||
(typeof import('@libreaudio/la-call'))['createWasmProcessor']
|
||||
|
||||
let wasmProcessorPromise: Promise<CreateWasmProcessor> | undefined
|
||||
|
||||
function loadWasmProcessor(): Promise<CreateWasmProcessor> {
|
||||
if (!wasmProcessorPromise) {
|
||||
wasmProcessorPromise = import('@libreaudio/la-call').then(
|
||||
(mod) => mod.createWasmProcessor
|
||||
)
|
||||
}
|
||||
return wasmProcessorPromise
|
||||
}
|
||||
|
||||
export interface AudioProcessorInterface extends TrackProcessor<Track.Kind.Audio> {
|
||||
name: string
|
||||
}
|
||||
@@ -20,7 +36,7 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
|
||||
private source?: MediaStreamTrack
|
||||
private sourceNode?: MediaStreamAudioSourceNode
|
||||
private destinationNode?: MediaStreamAudioDestinationNode
|
||||
private noiseSuppressionNode?: AudioWorkletNode
|
||||
private noiseSuppressionNode?: AudioNode
|
||||
|
||||
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
|
||||
if (!opts.track) {
|
||||
@@ -35,25 +51,17 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
|
||||
await audioContext.resume()
|
||||
}
|
||||
|
||||
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
|
||||
|
||||
this.sourceNode = audioContext.createMediaStreamSource(
|
||||
new MediaStream([this.source])
|
||||
)
|
||||
|
||||
this.noiseSuppressionNode = new AudioWorkletNode(
|
||||
audioContext,
|
||||
NoiseSuppressorWorklet_Name,
|
||||
{
|
||||
// RNNoise is a mono algorithm. Its worklet only denoises and writes
|
||||
// channel 0. Force any (possibly stereo) input to down-mix to a single
|
||||
// channel and emit a single channel, so we don't produce a stereo frame
|
||||
// whose right channel is left silent.
|
||||
channelCount: 1,
|
||||
channelCountMode: 'explicit',
|
||||
outputChannelCount: [1],
|
||||
}
|
||||
)
|
||||
const createWasmProcessor = await loadWasmProcessor()
|
||||
this.noiseSuppressionNode = await createWasmProcessor(audioContext, {
|
||||
intensity: 90,
|
||||
})
|
||||
if (!this.noiseSuppressionNode) {
|
||||
throw new Error('Failed to create Wasm processor')
|
||||
}
|
||||
|
||||
this.destinationNode = audioContext.createMediaStreamDestination()
|
||||
|
||||
|
||||
@@ -39,6 +39,19 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
type="switch"
|
||||
label={t('preferences.autoMuteLargeRoom.label')}
|
||||
description={t('preferences.autoMuteLargeRoom.description')}
|
||||
isSelected={userPreferencesSnap.is_auto_mute_large_room_enabled}
|
||||
onChange={(value) =>
|
||||
(userPreferencesStore.is_auto_mute_large_room_enabled = value)
|
||||
}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
|
||||
|
||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
||||
import { logout } from '@/features/auth/utils/logout'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const Logo = () => (
|
||||
<img
|
||||
@@ -84,6 +85,16 @@ const LoginHint = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const HIDE_LOGIN_PARAM = 'hideLogin'
|
||||
|
||||
const isLoginButtonHidden = () => {
|
||||
if (typeof window === 'undefined') return false
|
||||
const value = new URLSearchParams(window.location.search).get(
|
||||
HIDE_LOGIN_PARAM
|
||||
)
|
||||
return value === 'true'
|
||||
}
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
const isHome = useMatchesRoute('home')
|
||||
@@ -92,6 +103,9 @@ export const Header = () => {
|
||||
const isTermsOfService = useMatchesRoute('termsOfService')
|
||||
const isRoom = useMatchesRoute('room')
|
||||
const { user, isLoggedIn } = useUser()
|
||||
|
||||
const loginButtonDisabledByUrl = useMemo(() => isLoginButtonHidden(), [])
|
||||
|
||||
const userLabel = user?.full_name || user?.email
|
||||
const loggedInTooltip = t('loggedInUserTooltip')
|
||||
const loggedInAriaLabel = userLabel
|
||||
@@ -152,7 +166,8 @@ export const Header = () => {
|
||||
!isHome &&
|
||||
!isLegalTerms &&
|
||||
!isAccessibility &&
|
||||
!isTermsOfService && (
|
||||
!isTermsOfService &&
|
||||
!loginButtonDisabledByUrl && (
|
||||
<>
|
||||
<LoginButton proConnectHint={false} />
|
||||
<LoginHint />
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Du hast begonnen zu sprechen – deine Hand wird daher automatisch gesenkt.",
|
||||
"dismiss": "Hand oben lassen"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "Sie haben an einer großen Besprechung teilgenommen. Ihr Mikrofon wurde automatisch stummgeschaltet.",
|
||||
"dismiss": "Stummschaltung aufheben"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} hat mit {{emoji}} reagiert"
|
||||
},
|
||||
|
||||
@@ -290,11 +290,14 @@
|
||||
"apply": "Ersetze deinen Hintergrund:",
|
||||
"personal": {
|
||||
"title": "Meine Hintergründe",
|
||||
"selectedLabel": "Hintergrund abwählen (er ist aktuell angewendet).",
|
||||
"apply": "Als Hintergrund verwenden.",
|
||||
"selectFileTooltip": "Wähle ein Bild aus, das als persönlicher Hintergrund verwendet werden soll",
|
||||
"notLoggedInWarning": "Du bist nicht angemeldet, der persönliche Hintergrund wird nicht von einem Meeting zum anderen gespeichert.",
|
||||
"warningUploadDisabled": "Persönliche Hintergründe werden derzeit nicht von einem Meeting zum anderen gespeichert.",
|
||||
"uploadLimitReached": "Du kannst keine weiteren persönlichen Hintergründe hinzufügen.",
|
||||
"uploadInProgress": "Datei wird hochgeladen…",
|
||||
"deleteLabel": "Benutzerdefinierten Hintergrund löschen.",
|
||||
"errors": {
|
||||
"close": "Schließen",
|
||||
"file_too_large": {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Meetings ohne Teilnehmende verlassen",
|
||||
"description": "Meetings nach einigen Minuten automatisch verlassen, wenn keine andere Person beitritt"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Mikrofon automatisch stummschalten",
|
||||
"description": "Dein Mikrofon wird automatisch stummgeschaltet, wenn du an einer großen Besprechung teilnimmst."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "It seems you have started speaking, so your hand will be lowered.",
|
||||
"dismiss": "Keep hand raised"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "You have joined a large meeting. Your microphone has been automatically muted.",
|
||||
"dismiss": "Unmute"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reacted with {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -290,11 +290,14 @@
|
||||
"apply": "Replace your background:",
|
||||
"personal": {
|
||||
"title": "My backgrounds",
|
||||
"selectedLabel": "Unselect background (it's currently applied).",
|
||||
"apply": "Use as background.",
|
||||
"selectFileTooltip": "Select an image file to use as a personal background",
|
||||
"notLoggedInWarning": "You are not logged-in, personal backgrounds won't be saved from one meeting to the other.",
|
||||
"warningUploadDisabled": "Personal backgrounds are currently not saved from one meeting to the other.",
|
||||
"uploadLimitReached": "You cannot upload more personal backgrounds.",
|
||||
"uploadInProgress": "Image is being uploaded…",
|
||||
"deleteLabel": "Delete background.",
|
||||
"errors": {
|
||||
"close": "Close",
|
||||
"file_too_large": {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Leave calls with no participants",
|
||||
"description": "Automatically leaves a call after a few minutes if no other participant joins"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Automatically mute my microphone",
|
||||
"description": "Automatically mute your microphone when joining a large meeting."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
@@ -21,8 +25,8 @@
|
||||
"disabled": "Microphone disabled"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Noise reduction",
|
||||
"heading": "Noise reduction",
|
||||
"label": "Advanced voice optimization",
|
||||
"heading": "Audio filters",
|
||||
"ariaLabel": {
|
||||
"enable": "Enable noise reduction",
|
||||
"disable": "Disable noise reduction"
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
|
||||
"dismiss": "Laisser la main levée"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "Vous venez de rejoindre une grande réunion. Votre micro a été automatiquement coupé.",
|
||||
"dismiss": "Rétablir le micro"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} a reagi avec {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -290,11 +290,14 @@
|
||||
"apply": "Remplacer votre arrière plan :",
|
||||
"personal": {
|
||||
"title": "Mes arrière-plans",
|
||||
"selectedLabel": "Ne plus utiliser comme arrière-plan (actuellement actif).",
|
||||
"apply": "Utiliser comme arrière plan.",
|
||||
"selectFileTooltip": "Sélectionnez une image à utiliser comme arrière-plan",
|
||||
"notLoggedInWarning": "Vous n'êtes pas connecté, l'arrière-plan personnel ne sera pas sauvegardé d'une réunion à l'autre.",
|
||||
"warningUploadDisabled": "Les arrière-plans personnels ne sont actuellement pas sauvegardés d'une réunion à l'autre.",
|
||||
"uploadLimitReached": "Vous ne pouvez pas ajouter plus d'arrière-plans personnels.",
|
||||
"uploadInProgress": "Image en cours d'envoi…",
|
||||
"deleteLabel": "Supprimer l'arrière-plan.",
|
||||
"errors": {
|
||||
"close": "Fermer",
|
||||
"file_too_large": {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Quitter les appels sans autre participant",
|
||||
"description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Couper automatiquement mon micro",
|
||||
"description": "Coupe automatiquement votre micro lorsque vous rejoignez une réunion de grande taille."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Het lijkt erop dat u bent begonnen te spreken, dus we laten uw hand zakken.",
|
||||
"dismiss": "Houdt uw hand opgestoken"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "U bent zojuist lid geworden van een grote vergadering. Uw microfoon is automatisch gedempt.",
|
||||
"dismiss": "Microfoon inschakelen"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reageerde met {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -290,11 +290,14 @@
|
||||
"apply": "Vervang je achtergrond:",
|
||||
"personal": {
|
||||
"title": "Mijn achtergronden",
|
||||
"selectedLabel": "Deselecteer de achtergrond (deze is momenteel toegepast).",
|
||||
"apply": "Gebruik als achtergrond.",
|
||||
"selectFileTooltip": "Selecteer een afbeeldingsbestand om te gebruiken als persoonlijke achtergrond",
|
||||
"notLoggedInWarning": "U bent niet ingelogd, persoonlijke achtergronden worden niet opgeslagen van de ene vergadering naar de andere.",
|
||||
"warningUploadDisabled": "Persoonlijke achtergronden worden momenteel niet opgeslagen van de ene vergadering naar de andere.",
|
||||
"uploadLimitReached": "U kunt geen persoonlijke achtergronden meer uploaden.",
|
||||
"uploadInProgress": "Afbeelding wordt geüpload…",
|
||||
"deleteLabel": "Aangepaste achtergrond verwijderen.",
|
||||
"errors": {
|
||||
"close": "Sluiten",
|
||||
"file_too_large": {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Verlaat oproepen zonder deelnemers",
|
||||
"description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Mijn microfoon automatisch dempen",
|
||||
"description": "Uw microfoon wordt automatisch gedempt wanneer u deelneemt aan een grote vergadering."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||
|
||||
type State = {
|
||||
is_idle_disconnect_modal_enabled: boolean
|
||||
is_auto_mute_large_room_enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
is_idle_disconnect_modal_enabled: true,
|
||||
is_auto_mute_large_room_enabled: true,
|
||||
}
|
||||
|
||||
function getUserPreferencesState(): State {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
@@ -233,6 +233,7 @@ def format_transcript(
|
||||
recording_datetime: str | None,
|
||||
owner_timezone: str | None,
|
||||
download_link: str | None,
|
||||
form_link: str | None,
|
||||
) -> tuple[str, str]:
|
||||
"""Format a transcription into readable content with a title.
|
||||
|
||||
@@ -250,6 +251,7 @@ def format_transcript(
|
||||
recording_datetime=recording_datetime,
|
||||
owner_timezone=owner_timezone,
|
||||
download_link=download_link,
|
||||
form_link=form_link,
|
||||
)
|
||||
|
||||
|
||||
@@ -346,6 +348,13 @@ def process_audio_transcribe_summarize_v2(
|
||||
task_id,
|
||||
)
|
||||
|
||||
form_base_url = settings.transcription_satisfaction_form_base_url
|
||||
form_link = (
|
||||
f"{form_base_url}?room_id={room}"
|
||||
if (form_base_url and metadata_filename is not None)
|
||||
else None
|
||||
)
|
||||
|
||||
# Format output
|
||||
content, title = format_transcript(
|
||||
transcription,
|
||||
@@ -355,6 +364,7 @@ def process_audio_transcribe_summarize_v2(
|
||||
recording_start_at,
|
||||
owner_timezone,
|
||||
download_link,
|
||||
form_link,
|
||||
)
|
||||
|
||||
submit_content(content, title, email, sub)
|
||||
|
||||
@@ -122,6 +122,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# Summary related settings
|
||||
is_summary_enabled: bool = True
|
||||
transcription_satisfaction_form_base_url: Optional[str] = None
|
||||
|
||||
# Sentry
|
||||
sentry_is_enabled: bool = False
|
||||
|
||||
@@ -25,6 +25,10 @@ Einige Punkte, die wir Ihnen empfehlen zu überprüfen:
|
||||
download_header_template=(
|
||||
"\n*[Laden Sie hier Ihre Aufnahme herunter (externer Link)]({download_link})*\n"
|
||||
),
|
||||
form_footer_template=(
|
||||
"\n\n*[Teilen Sie uns mit, wie Ihnen diese Transkription gefallen hat "
|
||||
"(externer Link)]({form_link})*\n"
|
||||
),
|
||||
hallucination_replacement_text="[Text konnte nicht transkribiert werden]",
|
||||
document_default_title="Transkription",
|
||||
document_title_template=(
|
||||
|
||||
@@ -25,6 +25,10 @@ A few things we recommend you check:
|
||||
download_header_template=(
|
||||
"\n*[Download your recording (external link)]({download_link})*\n"
|
||||
),
|
||||
form_footer_template=(
|
||||
"\n\n*[Tell us what you thought of this transcription "
|
||||
"(external link)]({form_link})*\n"
|
||||
),
|
||||
hallucination_replacement_text="[Unable to transcribe text]",
|
||||
document_default_title="Transcription",
|
||||
document_title_template=(
|
||||
|
||||
@@ -25,6 +25,9 @@ Quelques points que nous vous conseillons de vérifier :
|
||||
download_header_template=(
|
||||
"\n*[Télécharger votre enregistrement Audio]({download_link})*\n"
|
||||
),
|
||||
form_footer_template=(
|
||||
"\n\n*[Donnez nous votre avis sur cette transcription]({form_link})*\n"
|
||||
),
|
||||
hallucination_replacement_text="[Texte impossible à transcrire]",
|
||||
document_default_title="Transcription",
|
||||
document_title_template=(
|
||||
|
||||
@@ -25,6 +25,10 @@ Een paar punten die wij u aanraden te controleren:
|
||||
download_header_template=(
|
||||
"\n*[Download hier je opname (externe link)]({download_link})*\n"
|
||||
),
|
||||
form_footer_template=(
|
||||
"\n\n*[Laat ons weten wat je van deze transcriptie vond "
|
||||
"(externe link)]({form_link})*\n"
|
||||
),
|
||||
hallucination_replacement_text="[Tekst kon niet worden getranscribeerd]",
|
||||
document_default_title="Transcriptie",
|
||||
document_title_template=(
|
||||
|
||||
@@ -10,6 +10,7 @@ class LocaleStrings:
|
||||
# transcript_formatter.py
|
||||
empty_transcription: str
|
||||
download_header_template: str
|
||||
form_footer_template: str
|
||||
hallucination_replacement_text: str
|
||||
document_default_title: str
|
||||
document_title_template: str
|
||||
|
||||
@@ -38,13 +38,14 @@ class TranscriptFormatter:
|
||||
|
||||
return None
|
||||
|
||||
def format(
|
||||
def format( # noqa: PLR0913
|
||||
self,
|
||||
transcription,
|
||||
room: str | None = None,
|
||||
recording_datetime: str | None = None,
|
||||
owner_timezone: str | None = None,
|
||||
download_link: str | None = None,
|
||||
form_link: str | None = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""Format transcription into the final document and its title."""
|
||||
segments = self._get_segments(transcription)
|
||||
@@ -55,6 +56,8 @@ class TranscriptFormatter:
|
||||
content = self._format_speaker(segments)
|
||||
content = self._remove_hallucinations(content)
|
||||
content = self._add_header(content, download_link)
|
||||
if form_link:
|
||||
content = self._add_footer(content, form_link)
|
||||
|
||||
title = self._generate_title(room, recording_datetime, owner_timezone)
|
||||
|
||||
@@ -93,9 +96,14 @@ class TranscriptFormatter:
|
||||
header = self._locale.download_header_template.format(
|
||||
download_link=download_link
|
||||
)
|
||||
content = header + content
|
||||
return header + content
|
||||
|
||||
return content
|
||||
def _add_footer(self, content, form_link: str | None):
|
||||
if not form_link:
|
||||
return content
|
||||
|
||||
footer = self._locale.form_footer_template.format(form_link=form_link)
|
||||
return content + footer
|
||||
|
||||
def _generate_title(
|
||||
self,
|
||||
|
||||
@@ -27,6 +27,7 @@ class TestTasks:
|
||||
"owner_timezone": "UTC",
|
||||
"language": None,
|
||||
"download_link": "https://example.com/file.mp4",
|
||||
"form_link": "https://satisfaction.com?room_id=test",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user