From 7a51b09664448bc0be11d9f58dbf1902b57916a4 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 15 Oct 2024 21:29:54 +0200 Subject: [PATCH 01/30] wip allow starting a recording from the frontend --- .../features/rooms/livekit/api/recordRoom.ts | 56 +++++++++++++++++++ .../controls/Options/OptionsMenuItems.tsx | 2 + .../controls/Options/RecordingMenuItem.tsx | 52 +++++++++++++++++ src/frontend/src/stores/egress.ts | 11 ++++ 4 files changed, 121 insertions(+) create mode 100644 src/frontend/src/features/rooms/livekit/api/recordRoom.ts create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/Options/RecordingMenuItem.tsx create mode 100644 src/frontend/src/stores/egress.ts diff --git a/src/frontend/src/features/rooms/livekit/api/recordRoom.ts b/src/frontend/src/features/rooms/livekit/api/recordRoom.ts new file mode 100644 index 00000000..c463c5d0 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/api/recordRoom.ts @@ -0,0 +1,56 @@ +import { fetchServerApi } from './fetchServerApi' +import { buildServerApiUrl } from './buildServerApiUrl' +import { useRoomData } from '../hooks/useRoomData' +import { useParams } from 'wouter' + +export const useRecordRoom = () => { + const data = useRoomData() + const { roomId: roomSlug } = useParams() + + const recordRoom = () => { + if (!data || !data?.livekit) { + throw new Error('Room data is not available') + } + if (!roomSlug) { + throw new Error('Room ID is not available') + } + return fetchServerApi( + buildServerApiUrl( + data.livekit.url, + '/twirp/livekit.Egress/StartRoomCompositeEgress' + ), + data.livekit.token, + { + method: 'POST', + body: JSON.stringify({ + room_name: data.livekit.room, + audio_only: true, + file_outputs: [ + { + file_extension: 'ogg', + filepath: `{room_name}_{time}_${roomSlug}`, + }, + ], + }), + } + ) + } + + const stopRecordingRoom = (egressId: string) => { + if (!data || !data?.livekit) { + throw new Error('Room data is not available') + } + return fetchServerApi( + buildServerApiUrl(data.livekit.url, '/twirp/livekit.Egress/StopEgress'), + data.livekit.token, + { + method: 'POST', + body: JSON.stringify({ + egressId, + }), + } + ) + } + + return { recordRoom, stopRecordingRoom } +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx index d84e4cb6..e7dc868c 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx @@ -10,6 +10,7 @@ import { Dispatch, SetStateAction } from 'react' import { DialogState } from './OptionsButton' import { Separator } from '@/primitives/Separator' import { useSidePanel } from '../../../hooks/useSidePanel' +import { RecordingMenuItem } from '@/features/rooms/livekit/components/controls/Options/RecordingMenuItem' // @todo try refactoring it to use MenuList component export const OptionsMenuItems = ({ @@ -34,6 +35,7 @@ export const OptionsMenuItems = ({ {t('effects')} +
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/RecordingMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/RecordingMenuItem.tsx new file mode 100644 index 00000000..935e8e8a --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/RecordingMenuItem.tsx @@ -0,0 +1,52 @@ +import { MenuItem } from 'react-aria-components' +import { menuItemRecipe } from '@/primitives/menuItemRecipe.ts' +import { RiPauseCircleLine, RiRecordCircleLine } from '@remixicon/react' +import { useRecordRoom } from '@/features/rooms/livekit/api/recordRoom.ts' +import { useState } from 'react' +import { egressStore } from '@/stores/egress.ts' +import { useSnapshot } from 'valtio' + +export const RecordingMenuItem = () => { + const { recordRoom, stopRecordingRoom } = useRecordRoom() + + const egressSnap = useSnapshot(egressStore) + const egressId = egressSnap.egressId + + const [isPending, setIsPending] = useState(false) + + const handleAction = async () => { + if (egressId) { + setIsPending(true) + egressStore.egressIsStopping = true + const response = await stopRecordingRoom(egressId) + console.log(response) + egressStore.egressId = undefined + setIsPending(false) + } else { + setIsPending(true) + const response = await recordRoom() + egressStore.egressId = response['egress_id'] as string + setIsPending(false) + } + } + + return ( + + {egressId ? ( + <> + + Arrêter l'enregistrement + + ) : ( + <> + + Enregistrer la réunion + + )} + + ) +} diff --git a/src/frontend/src/stores/egress.ts b/src/frontend/src/stores/egress.ts new file mode 100644 index 00000000..a06270bc --- /dev/null +++ b/src/frontend/src/stores/egress.ts @@ -0,0 +1,11 @@ +import { proxy } from 'valtio' + +type State = { + egressId: string | undefined + egressIsStopping: boolean +} + +export const egressStore = proxy({ + egressId: undefined, + egressIsStopping: false, +}) From d200eeb6bd5943bf8d85b9cf5568519ee03758bf Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 15 Oct 2024 21:31:40 +0200 Subject: [PATCH 02/30] wip grant recording permission --- src/backend/core/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index 304b5de7..56ac4b0d 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -53,6 +53,7 @@ def generate_token(room: str, user, username: Optional[str] = None) -> str: room=room, room_join=True, room_admin=True, + room_record=True, can_update_own_metadata=True, can_publish_sources=[ "camera", From 847729647103232f62a0dd6a75c25d91ee20f63f Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 15 Oct 2024 21:54:18 +0200 Subject: [PATCH 03/30] wip add recording indicator --- .../rooms/components/RecordingIndicator.tsx | 53 +++++++++++++++++++ .../rooms/livekit/prefabs/VideoConference.tsx | 2 + 2 files changed, 55 insertions(+) create mode 100644 src/frontend/src/features/rooms/components/RecordingIndicator.tsx diff --git a/src/frontend/src/features/rooms/components/RecordingIndicator.tsx b/src/frontend/src/features/rooms/components/RecordingIndicator.tsx new file mode 100644 index 00000000..675ef7bc --- /dev/null +++ b/src/frontend/src/features/rooms/components/RecordingIndicator.tsx @@ -0,0 +1,53 @@ +import { useRoomContext } from '@livekit/components-react' +import { useEffect, useState } from 'react' +import { RoomEvent } from 'livekit-client' +import { egressStore } from '@/stores/egress.ts' +import { useSnapshot } from 'valtio' + +export const RecordingIndicator = () => { + const room = useRoomContext() + const [isRecording, setIsRecording] = useState(room.isRecording) + + const egressSnap = useSnapshot(egressStore) + const egressIsStopping = egressSnap.egressIsStopping + + useEffect(() => { + const handleRecordingStatusChanges = (isRecording: boolean) => { + if (!isRecording) { + egressStore.egressIsStopping = false + } + + setIsRecording(isRecording) + } + room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges) + return () => { + room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges) + } + }, [room]) + + const getStatus = () => { + if (egressIsStopping) { + return 'saving recording' + } + if (isRecording) { + return 'recording' + } + if (!isRecording) { + return 'available' + } + } + + return ( +
+ Room status: {getStatus()} +
+ ) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index ff81b297..c7b203dc 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -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 { RecordingIndicator } from '@/features/rooms/components/RecordingIndicator.tsx' const LayoutWrapper = styled( 'div', @@ -165,6 +166,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { transition: 'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms', }} > +
Date: Wed, 16 Oct 2024 10:03:21 +0200 Subject: [PATCH 04/30] wip deploy to staging --- src/helm/env.d/staging/values.meet.yaml.gotmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index fa090c39..58b43590 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -1,7 +1,7 @@ image: repository: lasuite/meet-backend pullPolicy: Always - tag: "main" + tag: "v-hackathon" backend: migrateJobAnnotations: @@ -110,7 +110,7 @@ frontend: image: repository: lasuite/meet-frontend pullPolicy: Always - tag: "main" + tag: "v-hackathon" ingress: enabled: true From e271c87a2081376040818786d94d91692b7a6818 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 10:07:58 +0200 Subject: [PATCH 05/30] wip draft a mini webhook --- src/backend/core/api/demo.py | 28 ++++++++++++++++++++++++++++ src/backend/core/urls.py | 3 ++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/backend/core/api/demo.py diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py new file mode 100644 index 00000000..d33f7728 --- /dev/null +++ b/src/backend/core/api/demo.py @@ -0,0 +1,28 @@ + +from rest_framework.decorators import api_view +from rest_framework.response import Response + +MINIO_BUCKET = "livekit-staging-livekit-egress" + +# todo - discuss retry policy if the webhook fail +@api_view(["POST"]) +def minio_webhook(request): + + data = request.data + + record = data["Records"][0] + s3 = record['s3'] + bucket = s3['bucket'] + bucket_name = bucket['name'] + object = s3['object'] + filename = object['key'] + + if bucket_name != MINIO_BUCKET: + return Response("Not interested in this bucket") + + if object['contentType'] != 'audio/ogg': + return Response("Not interested in this file type") + + print('file received', filename) + + return Response("") diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index f7f0c5a9..2786f58b 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -5,7 +5,7 @@ from rest_framework.routers import DefaultRouter -from core.api import get_frontend_configuration, viewsets +from core.api import get_frontend_configuration, viewsets, demo from core.authentication.urls import urlpatterns as oidc_urls # - Main endpoints @@ -24,6 +24,7 @@ *router.urls, *oidc_urls, path("config/", get_frontend_configuration, name="config"), + path("minio-webhook/", demo.minio_webhook, name="demo"), ] ), ), From 5e57647b34ff9ce49ffeecd152a811a513267742 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 10:22:41 +0200 Subject: [PATCH 06/30] wip connect minio --- src/backend/core/api/demo.py | 13 +++++++++++++ src/backend/meet/settings.py | 10 ++++++++++ src/backend/pyproject.toml | 1 + src/helm/env.d/staging/values.meet.yaml.gotmpl | 12 ++++++++++++ src/helm/meet/templates/secrets.yaml | 9 +++++++++ 5 files changed, 45 insertions(+) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index d33f7728..fbd31126 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -1,6 +1,8 @@ from rest_framework.decorators import api_view from rest_framework.response import Response +from minio import Minio +from django.conf import settings MINIO_BUCKET = "livekit-staging-livekit-egress" @@ -25,4 +27,15 @@ def minio_webhook(request): print('file received', filename) + client = Minio( + settings.MINIO_URL, + access_key=settings.MINIO_ACCESS_KEY, + secret_key=settings.MINIO_SECRET_KEY, + ) + + room_id = filename.split("_")[2].split(".")[0] + + print('room_id', room_id, filename) + + return Response("") diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index bf2deaa5..ba7d5fa1 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -386,6 +386,16 @@ class Base(Configuration): None, environ_name="ANALYTICS_KEY", environ_prefix=None ) + # todo - totally wip + MINIO_ACCESS_KEY = values.Value( + None, environ_name="MINIO_ACCESS_KEY", environ_prefix=None + ) + MINIO_SECRET_KEY = values.Value( + None, environ_name="MINIO_SECRET_KEY", environ_prefix=None + ) + MINIO_URL = values.Value( + None, environ_name="MINIO_URL", environ_prefix=None + ) # pylint: disable=invalid-name @property def ENVIRONMENT(self): diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index c71c5e78..6af2344b 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -58,6 +58,7 @@ dependencies = [ "whitenoise==6.7.0", "mozilla-django-oidc==4.0.1", "livekit-api==0.7.0", + "minio==7.2.9", ] [project.urls] diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 58b43590..6eedb61e 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -97,6 +97,18 @@ backend: ALLOW_UNREGISTERED_ROOMS: False FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio-staging.beta.numerique.gouv.fr'}" FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}" + MINIO_ACCESS_KEY: + secretKeyRef: + name: backend + key: MINIO_ACCESS_KEY + MINIO_SECRET_KEY: + secretKeyRef: + name: backend + key: MINIO_SECRET_KEY + MINIO_URL: + secretKeyRef: + name: backend + key: MINIO_URL createsuperuser: command: diff --git a/src/helm/meet/templates/secrets.yaml b/src/helm/meet/templates/secrets.yaml index 14cf213d..44b2c5e4 100644 --- a/src/helm/meet/templates/secrets.yaml +++ b/src/helm/meet/templates/secrets.yaml @@ -15,3 +15,12 @@ stringData: OIDC_RP_CLIENT_SECRET: {{ .Values.oidc.clientSecret }} LIVEKIT_API_SECRET: {{ .Values.livekitApi.secret }} LIVEKIT_API_KEY: {{ .Values.livekitApi.key }} +{{- if .Values.minioAccessKey }} + MINIO_ACCESS_KEY: {{ .Values.minioAccessKey }} +{{- end }} +{{- if .Values.minioSecretKey }} + MINIO_SECRET_KEY: {{ .Values.minioSecretKey }} +{{- end }} +{{- if .Values.minioUrl }} + MINIO_URL: {{ .Values.minioUrl }} +{{- end }} From 3614b1e803690e82e18904a0f6a2d958d67d0f01 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 10:23:01 +0200 Subject: [PATCH 07/30] wip create openai client --- src/backend/core/api/demo.py | 5 +++++ src/backend/meet/settings.py | 4 ++++ src/backend/pyproject.toml | 1 + src/helm/env.d/staging/values.meet.yaml.gotmpl | 4 ++++ src/helm/meet/templates/secrets.yaml | 3 +++ 5 files changed, 17 insertions(+) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index fbd31126..2e4d7e66 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -3,9 +3,14 @@ from rest_framework.response import Response from minio import Minio from django.conf import settings +import openai MINIO_BUCKET = "livekit-staging-livekit-egress" +openai_client = openai.OpenAI( + api_key=settings.OPENAI_API_KEY, +) + # todo - discuss retry policy if the webhook fail @api_view(["POST"]) def minio_webhook(request): diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index ba7d5fa1..991377ea 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -396,6 +396,10 @@ class Base(Configuration): MINIO_URL = values.Value( None, environ_name="MINIO_URL", environ_prefix=None ) + OPENAI_API_KEY = values.Value( + None, environ_name="OPENAI_API_KEY", environ_prefix=None + ) + # pylint: disable=invalid-name @property def ENVIRONMENT(self): diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 6af2344b..9a78884e 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -59,6 +59,7 @@ dependencies = [ "mozilla-django-oidc==4.0.1", "livekit-api==0.7.0", "minio==7.2.9", + "openai==1.51.2" ] [project.urls] diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 6eedb61e..b4a1a538 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -109,6 +109,10 @@ backend: secretKeyRef: name: backend key: MINIO_URL + OPENAI_API_KEY: + secretKeyRef: + name: backend + key: OPENAI_API_KEY createsuperuser: command: diff --git a/src/helm/meet/templates/secrets.yaml b/src/helm/meet/templates/secrets.yaml index 44b2c5e4..47f4bb2d 100644 --- a/src/helm/meet/templates/secrets.yaml +++ b/src/helm/meet/templates/secrets.yaml @@ -24,3 +24,6 @@ stringData: {{- if .Values.minioUrl }} MINIO_URL: {{ .Values.minioUrl }} {{- end }} +{{- if .Values.openaiApiKey }} + OPENAI_API_KEY: {{ .Values.openaiApiKey }} +{{- end }} From 3dcc93b630da705bb0b15cabbeef39ed6e1c4e6e Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 10:34:17 +0200 Subject: [PATCH 08/30] wip feedbacks --- src/backend/core/api/demo.py | 9 ++++---- src/backend/meet/settings.py | 21 ++++++++++++----- .../env.d/production/values.meet.yaml.gotmpl | 17 ++++++++++++++ .../env.d/staging/values.meet.yaml.gotmpl | 23 +++++++++++-------- src/helm/extra/templates/s3.yml | 8 +++++++ src/helm/meet/templates/secrets.yaml | 9 -------- 6 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 src/helm/extra/templates/s3.yml diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 2e4d7e66..d775a1fd 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -5,7 +5,6 @@ from django.conf import settings import openai -MINIO_BUCKET = "livekit-staging-livekit-egress" openai_client = openai.OpenAI( api_key=settings.OPENAI_API_KEY, @@ -24,7 +23,7 @@ def minio_webhook(request): object = s3['object'] filename = object['key'] - if bucket_name != MINIO_BUCKET: + if bucket_name != settings.AWS_STORAGE_BUCKET_NAME: return Response("Not interested in this bucket") if object['contentType'] != 'audio/ogg': @@ -33,9 +32,9 @@ def minio_webhook(request): print('file received', filename) client = Minio( - settings.MINIO_URL, - access_key=settings.MINIO_ACCESS_KEY, - secret_key=settings.MINIO_SECRET_KEY, + settings.AWS_S3_ENDPOINT_URL, + access_key=settings.AWS_S3_ACCESS_KEY_ID, + secret_key=settings.AWS_S3_SECRET_ACCESS_KEY, ) room_id = filename.split("_")[2].split(".")[0] diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 991377ea..d43f8b3d 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -387,15 +387,24 @@ class Base(Configuration): ) # todo - totally wip - MINIO_ACCESS_KEY = values.Value( - None, environ_name="MINIO_ACCESS_KEY", environ_prefix=None + AWS_S3_ENDPOINT_URL = values.Value( + environ_name="AWS_S3_ENDPOINT_URL", environ_prefix=None ) - MINIO_SECRET_KEY = values.Value( - None, environ_name="MINIO_SECRET_KEY", environ_prefix=None + AWS_S3_ACCESS_KEY_ID = values.Value( + environ_name="AWS_S3_ACCESS_KEY_ID", environ_prefix=None ) - MINIO_URL = values.Value( - None, environ_name="MINIO_URL", environ_prefix=None + AWS_S3_SECRET_ACCESS_KEY = values.Value( + environ_name="AWS_S3_SECRET_ACCESS_KEY", environ_prefix=None ) + AWS_S3_REGION_NAME = values.Value( + environ_name="AWS_S3_REGION_NAME", environ_prefix=None + ) + AWS_STORAGE_BUCKET_NAME = values.Value( + "meet-media-storage", + environ_name="AWS_STORAGE_BUCKET_NAME", + environ_prefix=None, + ) + OPENAI_API_KEY = values.Value( None, environ_name="OPENAI_API_KEY", environ_prefix=None ) diff --git a/src/helm/env.d/production/values.meet.yaml.gotmpl b/src/helm/env.d/production/values.meet.yaml.gotmpl index 8844ddbd..75b9b198 100644 --- a/src/helm/env.d/production/values.meet.yaml.gotmpl +++ b/src/helm/env.d/production/values.meet.yaml.gotmpl @@ -99,6 +99,23 @@ backend: FRONTEND_SILENCE_LIVEKIT_DEBUG: False FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio.numerique.gouv.fr'}" FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}" + AWS_S3_ENDPOINT_URL: + secretKeyRef: + name: impress-media-storage.bucket.libre.sh + key: url + AWS_S3_ACCESS_KEY_ID: + secretKeyRef: + name: impress-media-storage.bucket.libre.sh + key: accessKey + AWS_S3_SECRET_ACCESS_KEY: + secretKeyRef: + name: impress-media-storage.bucket.libre.sh + key: secretKey + AWS_STORAGE_BUCKET_NAME: + secretKeyRef: + name: impress-media-storage.bucket.libre.sh + key: bucket + AWS_S3_REGION_NAME: local createsuperuser: command: diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index b4a1a538..8b5b24d5 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -97,18 +97,23 @@ backend: ALLOW_UNREGISTERED_ROOMS: False FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio-staging.beta.numerique.gouv.fr'}" FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}" - MINIO_ACCESS_KEY: + AWS_S3_ENDPOINT_URL: secretKeyRef: - name: backend - key: MINIO_ACCESS_KEY - MINIO_SECRET_KEY: + name: meet-media-storage.bucket.libre.sh + key: url + AWS_S3_ACCESS_KEY_ID: secretKeyRef: - name: backend - key: MINIO_SECRET_KEY - MINIO_URL: + name: meet-media-storage.bucket.libre.sh + key: accessKey + AWS_S3_SECRET_ACCESS_KEY: secretKeyRef: - name: backend - key: MINIO_URL + name: meet-media-storage.bucket.libre.sh + key: secretKey + AWS_STORAGE_BUCKET_NAME: + secretKeyRef: + name: meet-media-storage.bucket.libre.sh + key: bucket + AWS_S3_REGION_NAME: local OPENAI_API_KEY: secretKeyRef: name: backend diff --git a/src/helm/extra/templates/s3.yml b/src/helm/extra/templates/s3.yml new file mode 100644 index 00000000..4ca831ea --- /dev/null +++ b/src/helm/extra/templates/s3.yml @@ -0,0 +1,8 @@ +apiVersion: core.libre.sh/v1alpha1 +kind: Bucket +metadata: + name: meet-media-storage + namespace: {{ .Release.Namespace | quote }} +spec: + provider: data + versioned: true \ No newline at end of file diff --git a/src/helm/meet/templates/secrets.yaml b/src/helm/meet/templates/secrets.yaml index 47f4bb2d..eff18ec8 100644 --- a/src/helm/meet/templates/secrets.yaml +++ b/src/helm/meet/templates/secrets.yaml @@ -15,15 +15,6 @@ stringData: OIDC_RP_CLIENT_SECRET: {{ .Values.oidc.clientSecret }} LIVEKIT_API_SECRET: {{ .Values.livekitApi.secret }} LIVEKIT_API_KEY: {{ .Values.livekitApi.key }} -{{- if .Values.minioAccessKey }} - MINIO_ACCESS_KEY: {{ .Values.minioAccessKey }} -{{- end }} -{{- if .Values.minioSecretKey }} - MINIO_SECRET_KEY: {{ .Values.minioSecretKey }} -{{- end }} -{{- if .Values.minioUrl }} - MINIO_URL: {{ .Values.minioUrl }} -{{- end }} {{- if .Values.openaiApiKey }} OPENAI_API_KEY: {{ .Values.openaiApiKey }} {{- end }} From ac183c9eb9a62fb281ec0e433f97dca17b9c92fe Mon Sep 17 00:00:00 2001 From: Jacques ROUSSEL Date: Wed, 16 Oct 2024 10:47:50 +0200 Subject: [PATCH 09/30] wip bump secrets --- secrets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secrets b/secrets index 8ef9f451..f7e39853 160000 --- a/secrets +++ b/secrets @@ -1 +1 @@ -Subproject commit 8ef9f4513a63e313fdadd0a06c6f85091dad1013 +Subproject commit f7e39853e321c632954f25fb9eefdd1ff68c22ff From e8618099acf4c365db71c2ce3718028ca414ec45 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 12:00:07 +0200 Subject: [PATCH 10/30] wip add some logs and allow toggling openai request --- src/backend/core/api/demo.py | 54 ++++++++++++++++--- src/backend/meet/settings.py | 17 ++++++ .../env.d/staging/values.meet.yaml.gotmpl | 1 + 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index d775a1fd..c0d57405 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -4,11 +4,14 @@ from minio import Minio from django.conf import settings import openai +import logging +import tempfile +import os + + +logger = logging.getLogger(__name__) -openai_client = openai.OpenAI( - api_key=settings.OPENAI_API_KEY, -) # todo - discuss retry policy if the webhook fail @api_view(["POST"]) @@ -16,6 +19,8 @@ def minio_webhook(request): data = request.data + logger.info('Minio webhook sent %s', data) + record = data["Records"][0] s3 = record['s3'] bucket = s3['bucket'] @@ -29,7 +34,8 @@ def minio_webhook(request): if object['contentType'] != 'audio/ogg': return Response("Not interested in this file type") - print('file received', filename) + room_id = filename.split("_")[2].split(".")[0] + logger.info('file received %s for room %s', filename, room_id) client = Minio( settings.AWS_S3_ENDPOINT_URL, @@ -37,9 +43,45 @@ def minio_webhook(request): secret_key=settings.AWS_S3_SECRET_ACCESS_KEY, ) - room_id = filename.split("_")[2].split(".")[0] + try: + logger.info('downloading file %s', filename) + audio_file_stream = client.get_object(settings.AWS_STORAGE_BUCKET_NAME, object_name=filename) + + with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: + for data in audio_file_stream.stream(32*1024): + temp_audio_file.write(data) + + temp_file_path = temp_audio_file.name + logger.info('Temporary file created at %s', temp_file_path) + + audio_file_stream.close() + audio_file_stream.release_conn() + + if settings.OPENAI_ENABLE: + + openai_client = openai.OpenAI( + api_key=settings.OPENAI_API_KEY, + ) + + with open(temp_file_path, "rb") as audio_file: + + logger.info('Querying transcription …') + + transcript = openai_client.audio.transcriptions.create( + model="whisper-1", + file=audio_file + ) + + logger.info(transcript) + + except Exception as e: + logger.error("An error occurred: %s", str(e)) + raise - print('room_id', room_id, filename) + finally: + if temp_file_path and os.path.exists(temp_file_path): + os.remove(temp_file_path) + logger.info("Temporary file %s has been deleted.", temp_file_path) return Response("") diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index d43f8b3d..dfdee8d6 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -408,6 +408,9 @@ class Base(Configuration): OPENAI_API_KEY = values.Value( None, environ_name="OPENAI_API_KEY", environ_prefix=None ) + OPENAI_ENABLE = values.BooleanValue( + True, environ_name="OPENAI_ENABLE", environ_prefix=None + ) # pylint: disable=invalid-name @property @@ -552,6 +555,20 @@ class Production(Base): ALLOWED_HOSTS=["foo.com", "foo.fr"] """ + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", + }, + } + # Security ALLOWED_HOSTS = [ *values.ListValue([], environ_name="ALLOWED_HOSTS"), diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 8b5b24d5..670c3410 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -118,6 +118,7 @@ backend: secretKeyRef: name: backend key: OPENAI_API_KEY + OPENAI_ENABLE: False createsuperuser: command: From 83cfcacc0ed92630f50677bb76d6ae0ca99075bf Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 12:14:39 +0200 Subject: [PATCH 11/30] wip add prompt and summary --- src/backend/core/api/demo.py | 54 +++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index c0d57405..d9ea2021 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -13,6 +13,42 @@ logger = logging.getLogger(__name__) +def get_prompt(transcript): + return f""" + You are a helpful assistant. + + Summarize the following meeting transcript into a structured meeting minute format without additional comments about the meeting itself. Organize the summary into multiple parts, omitting the parts that are not applicable: + + 1. Summary: Provide a short summary of the entire conversation. + 2. Subjects Discussed: Provide a concise list of the main topics or issues covered during the meeting. + 3. Decisions Taken: Clearly and concisely state the decisions or resolutions that were agreed upon in short bullet points. Ensure that all decisions are well-defined and actionable. + 4. Next Steps: List action items or tasks in brief bullet points, including the responsible persons and deadlines (if mentioned). Make sure no action item is left unassigned or without a deadline if one is mentioned. + + If any part of the transcript is unclear or requires further precision, please notify the user gently, suggesting specific areas that may need additional information. Review everything carefully, make sure not to make unsubstantiated claims. + + Please keep proper markdown title and formatting in the answer. + + {transcript} + + Answer: + + ## Summary + [provide a summary of the entire conversation] + + ## Subjects Discussed: + - [Concise bullet point summarizing subject] + - [Concise bullet point summarizing subject] + + ## Decisions Taken: + - [Clear and actionable decision] + - [Clear and actionable decision] + + ## Next Steps: + - [Action item or task] - [Responsible person, if applicable] - [Deadline, if applicable] + - [Action item or task] - [Responsible person, if applicable] - [Deadline, if applicable] + """ + + # todo - discuss retry policy if the webhook fail @api_view(["POST"]) def minio_webhook(request): @@ -72,7 +108,23 @@ def minio_webhook(request): file=audio_file ) - logger.info(transcript) + logger.info('Transcript: %s', transcript) + + prompt = get_prompt(transcript) + + logger.info('Prompt: %s', prompt) + + summary_response = openai_client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": "You are an expert assistant that summarizes meeting transcripts."}, + {"role": "user", "content": prompt} + ], + max_tokens=150 # todo - dig what does this parameter + ) + + summary = summary_response.choices[0].message.content + logger.info('Summary: %s', summary) except Exception as e: logger.error("An error occurred: %s", str(e)) From 41c9693d107a20a430f56bdf62df10d2cf3afb08 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 12:14:56 +0200 Subject: [PATCH 12/30] wip handle if file doesn't exist --- src/backend/core/api/demo.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index d9ea2021..1703b125 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -79,21 +79,30 @@ def minio_webhook(request): secret_key=settings.AWS_S3_SECRET_ACCESS_KEY, ) + temp_file_path = None + try: logger.info('downloading file %s', filename) - audio_file_stream = client.get_object(settings.AWS_STORAGE_BUCKET_NAME, object_name=filename) + + try: + audio_file_stream = client.get_object(settings.AWS_STORAGE_BUCKET_NAME, object_name=filename) + + with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: + for data in audio_file_stream.stream(32*1024): + temp_audio_file.write(data) - with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: - for data in audio_file_stream.stream(32*1024): - temp_audio_file.write(data) + temp_file_path = temp_audio_file.name + logger.info('Temporary file created at %s', temp_file_path) - temp_file_path = temp_audio_file.name - logger.info('Temporary file created at %s', temp_file_path) + audio_file_stream.close() + audio_file_stream.release_conn() - audio_file_stream.close() - audio_file_stream.release_conn() + except Exception as e: + + logger.error("An error occurred while accessing the object: %s", str(e)) + return Response("Error accessing file", status=500) - if settings.OPENAI_ENABLE: + if settings.OPENAI_ENABLE and temp_file_path: openai_client = openai.OpenAI( api_key=settings.OPENAI_API_KEY, From f0b250739c25a06b2969c2fb2babbdc05fef86fe Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 12:16:08 +0200 Subject: [PATCH 13/30] wip get room from filename --- src/backend/core/api/demo.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 1703b125..e01369c6 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -5,6 +5,7 @@ from django.conf import settings import openai import logging +from ..models import Room, RoleChoices import tempfile import os @@ -70,8 +71,8 @@ def minio_webhook(request): if object['contentType'] != 'audio/ogg': return Response("Not interested in this file type") - room_id = filename.split("_")[2].split(".")[0] - logger.info('file received %s for room %s', filename, room_id) + room_slug = filename.split("_")[2].split(".")[0] + logger.info('file received %s for room %s', filename, room_slug) client = Minio( settings.AWS_S3_ENDPOINT_URL, @@ -144,5 +145,14 @@ def minio_webhook(request): os.remove(temp_file_path) logger.info("Temporary file %s has been deleted.", temp_file_path) + try: + room = Room.objects.get(slug=room_slug) + owner_accesses = room.accesses.filter(role=RoleChoices.OWNER) + owners = [access.user for access in owner_accesses] + logger.info("Room %s has owners: %s", room_slug, owners) + + except Room.DoesNotExist: + logger.error("Room with slug %s does not exist", room_slug) + owners = [] return Response("") From c9c5d3b4523ae79cfb7eab8576e4c31f5e16c458 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 13:26:47 +0200 Subject: [PATCH 14/30] wip debug bucket --- src/backend/core/api/demo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index e01369c6..6156d246 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -65,10 +65,15 @@ def minio_webhook(request): object = s3['object'] filename = object['key'] - if bucket_name != settings.AWS_STORAGE_BUCKET_NAME: + logger.info(settings.AWS_STORAGE_BUCKET_NAME) + + # if bucket_name != settings.AWS_STORAGE_BUCKET_NAME: + if bucket_name != 'livekit-staging-livekit-egress': + logger.info('Not interested in this bucket: %s', bucket_name) return Response("Not interested in this bucket") if object['contentType'] != 'audio/ogg': + logger.info('Not interested in this file type: %s', object['contentType']) return Response("Not interested in this file type") room_slug = filename.split("_")[2].split(".")[0] From 0cf5ab1825cba223132a59913d24d091e31bd97b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 13:40:12 +0200 Subject: [PATCH 15/30] wip --- src/backend/core/api/demo.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 6156d246..677e5d79 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -65,10 +65,7 @@ def minio_webhook(request): object = s3['object'] filename = object['key'] - logger.info(settings.AWS_STORAGE_BUCKET_NAME) - - # if bucket_name != settings.AWS_STORAGE_BUCKET_NAME: - if bucket_name != 'livekit-staging-livekit-egress': + if bucket_name != settings.AWS_STORAGE_BUCKET_NAME: logger.info('Not interested in this bucket: %s', bucket_name) return Response("Not interested in this bucket") @@ -79,11 +76,14 @@ def minio_webhook(request): room_slug = filename.split("_")[2].split(".")[0] logger.info('file received %s for room %s', filename, room_slug) - client = Minio( - settings.AWS_S3_ENDPOINT_URL, - access_key=settings.AWS_S3_ACCESS_KEY_ID, - secret_key=settings.AWS_S3_SECRET_ACCESS_KEY, - ) + try: + client = Minio( + settings.AWS_S3_ENDPOINT_URL, + access_key=settings.AWS_S3_ACCESS_KEY_ID, + secret_key=settings.AWS_S3_SECRET_ACCESS_KEY, + ) + except Exception as e: + logger.error("An error occurred while creating the Minio client %s: %s", settings.AWS_S3_ENDPOINT_URL, str(e)) temp_file_path = None @@ -91,6 +91,7 @@ def minio_webhook(request): logger.info('downloading file %s', filename) try: + audio_file_stream = client.get_object(settings.AWS_STORAGE_BUCKET_NAME, object_name=filename) with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: @@ -104,7 +105,6 @@ def minio_webhook(request): audio_file_stream.release_conn() except Exception as e: - logger.error("An error occurred while accessing the object: %s", str(e)) return Response("Error accessing file", status=500) From fdaf567f5d9a4be7d192d433bb7903ae2b312739 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 14:41:41 +0200 Subject: [PATCH 16/30] wip revert minio --- src/backend/core/api/demo.py | 12 ++++++------ src/backend/meet/settings.py | 14 ++++++++++++++ src/helm/env.d/staging/values.meet.yaml.gotmpl | 13 +++++++++++++ src/helm/meet/templates/secrets.yaml | 10 ++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 677e5d79..27b9e0b2 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -65,7 +65,7 @@ def minio_webhook(request): object = s3['object'] filename = object['key'] - if bucket_name != settings.AWS_STORAGE_BUCKET_NAME: + if bucket_name != settings.MINIO_BUCKET: logger.info('Not interested in this bucket: %s', bucket_name) return Response("Not interested in this bucket") @@ -78,12 +78,12 @@ def minio_webhook(request): try: client = Minio( - settings.AWS_S3_ENDPOINT_URL, - access_key=settings.AWS_S3_ACCESS_KEY_ID, - secret_key=settings.AWS_S3_SECRET_ACCESS_KEY, + settings.MINIO_URL, + access_key=settings.MINIO_ACCESS_KEY, + secret_key=settings.MINIO_SECRET_KEY, ) except Exception as e: - logger.error("An error occurred while creating the Minio client %s: %s", settings.AWS_S3_ENDPOINT_URL, str(e)) + logger.error("An error occurred while creating the Minio client %s: %s", settings.MINIO_URL, str(e)) temp_file_path = None @@ -92,7 +92,7 @@ def minio_webhook(request): try: - audio_file_stream = client.get_object(settings.AWS_STORAGE_BUCKET_NAME, object_name=filename) + audio_file_stream = client.get_object(settings.MINIO_BUCKET, object_name=filename) with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: for data in audio_file_stream.stream(32*1024): diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index dfdee8d6..4682f9f5 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -412,6 +412,20 @@ class Base(Configuration): True, environ_name="OPENAI_ENABLE", environ_prefix=None ) + # todo - totally wip + MINIO_ACCESS_KEY = values.Value( + None, environ_name="MINIO_ACCESS_KEY", environ_prefix=None + ) + MINIO_SECRET_KEY = values.Value( + None, environ_name="MINIO_SECRET_KEY", environ_prefix=None + ) + MINIO_URL = values.Value( + None, environ_name="MINIO_URL", environ_prefix=None + ) + MINIO_BUCKET = values.Value( + 'livekit-staging-livekit-egress', environ_name="MINIO_BUCKET", environ_prefix=None + ) + # pylint: disable=invalid-name @property def ENVIRONMENT(self): diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 670c3410..0e3e24ed 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -119,6 +119,19 @@ backend: name: backend key: OPENAI_API_KEY OPENAI_ENABLE: False + MINIO_ACCESS_KEY: + secretKeyRef: + name: backend + key: MINIO_ACCESS_KEY + MINIO_SECRET_KEY: + secretKeyRef: + name: backend + key: MINIO_SECRET_KEY + MINIO_URL: + secretKeyRef: + name: backend + key: MINIO_URL + createsuperuser: command: diff --git a/src/helm/meet/templates/secrets.yaml b/src/helm/meet/templates/secrets.yaml index eff18ec8..adae816f 100644 --- a/src/helm/meet/templates/secrets.yaml +++ b/src/helm/meet/templates/secrets.yaml @@ -18,3 +18,13 @@ stringData: {{- if .Values.openaiApiKey }} OPENAI_API_KEY: {{ .Values.openaiApiKey }} {{- end }} +{{- if .Values.minioAccessKey }} + MINIO_ACCESS_KEY: {{ .Values.minioAccessKey }} +{{- end }} +{{- if .Values.minioSecretKey }} + MINIO_SECRET_KEY: {{ .Values.minioSecretKey }} +{{- end }} +{{- if .Values.minioUrl }} + MINIO_URL: {{ .Values.minioUrl }} +{{- end }} + From 817cf25b37312b639e8ca879b0307e27ca4b0dc4 Mon Sep 17 00:00:00 2001 From: Jacques ROUSSEL Date: Wed, 16 Oct 2024 14:57:15 +0200 Subject: [PATCH 17/30] wip bump secret --- secrets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secrets b/secrets index f7e39853..2ef26100 160000 --- a/secrets +++ b/secrets @@ -1 +1 @@ -Subproject commit f7e39853e321c632954f25fb9eefdd1ff68c22ff +Subproject commit 2ef26100715e0cc7093d44aaed54ed5a9c68cb13 From 2a56cda55c63e589b05e9ca453b47ef82cbef98e Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 15:07:27 +0200 Subject: [PATCH 18/30] wip enable openai --- src/helm/env.d/staging/values.meet.yaml.gotmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 0e3e24ed..23432335 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -118,7 +118,7 @@ backend: secretKeyRef: name: backend key: OPENAI_API_KEY - OPENAI_ENABLE: False + OPENAI_ENABLE: True MINIO_ACCESS_KEY: secretKeyRef: name: backend From a4820ae867fcc5ed1bfa969b4caeaf047997181c Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 17:47:32 +0200 Subject: [PATCH 19/30] wip email user --- src/backend/core/api/demo.py | 22 +++++++++++++++--- src/backend/core/models.py | 23 +++++++++++++++++++ .../env.d/staging/values.meet.yaml.gotmpl | 3 ++- src/mail/mjml/summary.mjml | 19 +++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/mail/mjml/summary.mjml diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 27b9e0b2..f9a1cfa9 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -9,6 +9,7 @@ import tempfile import os +import smtplib logger = logging.getLogger(__name__) @@ -106,7 +107,7 @@ def minio_webhook(request): except Exception as e: logger.error("An error occurred while accessing the object: %s", str(e)) - return Response("Error accessing file", status=500) + return Response("") if settings.OPENAI_ENABLE and temp_file_path: @@ -151,13 +152,28 @@ def minio_webhook(request): logger.info("Temporary file %s has been deleted.", temp_file_path) try: - room = Room.objects.get(slug=room_slug) + room = Room.objects.get(slug="fqj-hvzr-ieh") owner_accesses = room.accesses.filter(role=RoleChoices.OWNER) owners = [access.user for access in owner_accesses] logger.info("Room %s has owners: %s", room_slug, owners) except Room.DoesNotExist: logger.error("Room with slug %s does not exist", room_slug) - owners = [] + owners = None + room = None + + if not owners or not room: + logger.error("No owners") + return Response("") + + # todo - get link + + try: + logger.info("Emailing owners: %s", owners) + room.email_summary(owners=owners, link="wip") + except smtplib.SMTPException: + logger.error("Error while emailing owners") + return Response("") + return Response("") diff --git a/src/backend/core/models.py b/src/backend/core/models.py index f2d4e77b..7b39f40a 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -15,6 +15,8 @@ from django.utils.text import capfirst, slugify from django.utils.translation import gettext_lazy as _ +from django.template.loader import render_to_string + from timezone_field import TimeZoneField logger = getLogger(__name__) @@ -325,3 +327,24 @@ def clean_fields(self, exclude=None): else: raise ValidationError({"name": f'Room name "{self.name:s}" is reserved.'}) super().clean_fields(exclude=exclude) + + + def email_summary(self, owners, link): + """Wip""" + + template_vars = { + "title": "Votre résumé est prêt", + "link": link, + "room": self.slug, + } + msg_html = render_to_string("mail/html/summary.html", template_vars) + msg_plain = render_to_string("mail/text/invitation.txt", template_vars) + + for owner in owners: + owner.email_user( + subject="Votre résumé est prêt", + from_email=settings.EMAIL_FROM, + message=msg_plain, + html_message=msg_html, + fail_silently=False, + ) diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 23432335..e5449204 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -27,6 +27,7 @@ backend: DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr" DJANGO_EMAIL_PORT: 465 DJANGO_EMAIL_USE_SSL: True + DJANGO_EMAIL_USE_TLS: True OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token @@ -118,7 +119,7 @@ backend: secretKeyRef: name: backend key: OPENAI_API_KEY - OPENAI_ENABLE: True + OPENAI_ENABLE: False MINIO_ACCESS_KEY: secretKeyRef: name: backend diff --git a/src/mail/mjml/summary.mjml b/src/mail/mjml/summary.mjml new file mode 100644 index 00000000..2e9d38a7 --- /dev/null +++ b/src/mail/mjml/summary.mjml @@ -0,0 +1,19 @@ + + + + + + + +

Cher utilisateur,

+

La réunion {{room}} a été transcrite et résumée avec succès.

+
+ + Obtenez votre résumé + +
+
+
+
+ +
From 376ff8982cec6e26020346c1d46366267c509a04 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 17:57:58 +0200 Subject: [PATCH 20/30] wip room_slug --- src/backend/core/api/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index f9a1cfa9..66dae704 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -152,7 +152,7 @@ def minio_webhook(request): logger.info("Temporary file %s has been deleted.", temp_file_path) try: - room = Room.objects.get(slug="fqj-hvzr-ieh") + room = Room.objects.get(slug=room_slug) owner_accesses = room.accesses.filter(role=RoleChoices.OWNER) owners = [access.user for access in owner_accesses] logger.info("Room %s has owners: %s", room_slug, owners) From 7105c7cbae082400f6d6dcb816e93df56ffafcb1 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 16 Oct 2024 18:04:44 +0200 Subject: [PATCH 21/30] remove tl --- src/helm/env.d/staging/values.meet.yaml.gotmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index e5449204..0e3e24ed 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -27,7 +27,6 @@ backend: DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr" DJANGO_EMAIL_PORT: 465 DJANGO_EMAIL_USE_SSL: True - DJANGO_EMAIL_USE_TLS: True OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token From 2f675c2a3fbf4cc8afd167c381eb243d38bb9c35 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 10:55:05 +0200 Subject: [PATCH 22/30] wip clean and get blocknote content --- src/backend/core/api/demo.py | 192 +++++++++++++++++++++++++---------- src/backend/meet/settings.py | 3 + 2 files changed, 144 insertions(+), 51 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 66dae704..c81b951d 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -10,6 +10,7 @@ import tempfile import os import smtplib +import requests logger = logging.getLogger(__name__) @@ -51,6 +52,128 @@ def get_prompt(transcript): """ + +def get_room_and_owners(slug): + """Wip.""" + + try: + room = Room.objects.get(slug=slug) + owner_accesses = room.accesses.filter(role=RoleChoices.OWNER) + owners = [access.user for access in owner_accesses] + + logger.info("Room %s has owners: %s", slug, owners) + + except Room.DoesNotExist: + logger.error("Room with slug %s does not exist", slug) + + owners = None + room = None + + return room, owners + + +def remove_temporary_file(path): + """Wip.""" + + if not path or not os.path.exists(path): + return + + os.remove(path) + logger.info("Temporary file %s has been deleted.", path) + +def get_blocknote_content(summary): + """Wip.""" + + if not settings.BLOCKNOTE_CONVERTER_URL: + logger.error("BLOCKNOTE_CONVERTER_URL is not configured") + return None + + headers = { + "Content-Type": "application/json" + } + + data = { + "markdown": summary + } + + logger.info("Converting summary in BlockNote.js…") + response = requests.post(settings.BLOCKNOTE_CONVERTER_URL, headers=headers, json=data) + + if response.status_code != 200: + logger.error(f"Failed to convert summary. Status code: {response.status_code}") + + response_data = response.json() + if not 'content' in response_data: + logger.error(f"Content is missing: %s", response_data) + + content = response_data['content'] + logger.info("Base64 content:", content) + + return content + + +def get_doc_link(content, email): + """Wip.""" + + logger.info("Wip create a document with content for %s", email) + + return "link" + +def email_owner_with_summary(room, link, owner): + """Wip.""" + + logger.info("Emailing owner: %s", owner) + + try: + room.email_summary(owners=[owner], link=link) + except smtplib.SMTPException: + logger.error("Error while emailing owner") + +def strip_room_slug(filename): + """Wip.""" + return filename.split("_")[2].split(".")[0] + + +def get_minio_client(): + """Wip.""" + + try: + return Minio( + settings.MINIO_URL, + access_key=settings.MINIO_ACCESS_KEY, + secret_key=settings.MINIO_SECRET_KEY, + ) + except Exception as e: + logger.error("An error occurred while creating the Minio client %s: %s", settings.MINIO_URL, str(e)) + + +def download_temporary_file(minio_client, filename): + """Wip.""" + + temp_file_path = None + + logger.info('downloading file %s', filename) + + try: + audio_file_stream = minio_client.get_object(settings.MINIO_BUCKET, object_name=filename) + + with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: + + for data in audio_file_stream.stream(32 * 1024): + temp_audio_file.write(data) + + temp_file_path = temp_audio_file.name + logger.info('Temporary file created at %s', temp_file_path) + + audio_file_stream.close() + audio_file_stream.release_conn() + + except Exception as e: + logger.error("An error occurred while accessing the object: %s", str(e)) + + return temp_file_path + + # todo - discuss retry policy if the webhook fail @api_view(["POST"]) def minio_webhook(request): @@ -74,58 +197,31 @@ def minio_webhook(request): logger.info('Not interested in this file type: %s', object['contentType']) return Response("Not interested in this file type") - room_slug = filename.split("_")[2].split(".")[0] + room_slug = strip_room_slug(filename) logger.info('file received %s for room %s', filename, room_slug) - try: - client = Minio( - settings.MINIO_URL, - access_key=settings.MINIO_ACCESS_KEY, - secret_key=settings.MINIO_SECRET_KEY, - ) - except Exception as e: - logger.error("An error occurred while creating the Minio client %s: %s", settings.MINIO_URL, str(e)) + minio_client = get_minio_client() temp_file_path = None + summary = None try: - logger.info('downloading file %s', filename) - - try: - - audio_file_stream = client.get_object(settings.MINIO_BUCKET, object_name=filename) - - with tempfile.NamedTemporaryFile(delete=False, suffix='.ogg') as temp_audio_file: - for data in audio_file_stream.stream(32*1024): - temp_audio_file.write(data) - - temp_file_path = temp_audio_file.name - logger.info('Temporary file created at %s', temp_file_path) - - audio_file_stream.close() - audio_file_stream.release_conn() - - except Exception as e: - logger.error("An error occurred while accessing the object: %s", str(e)) - return Response("") + temp_file_path = download_temporary_file(minio_client, filename) if settings.OPENAI_ENABLE and temp_file_path: - + logger.info('Initiating OpenAI client …') openai_client = openai.OpenAI( api_key=settings.OPENAI_API_KEY, ) with open(temp_file_path, "rb") as audio_file: - logger.info('Querying transcription …') - transcript = openai_client.audio.transcriptions.create( model="whisper-1", file=audio_file ) logger.info('Transcript: %s', transcript) - prompt = get_prompt(transcript) logger.info('Prompt: %s', prompt) @@ -147,33 +243,27 @@ def minio_webhook(request): raise finally: - if temp_file_path and os.path.exists(temp_file_path): - os.remove(temp_file_path) - logger.info("Temporary file %s has been deleted.", temp_file_path) + remove_temporary_file(temp_file_path) - try: - room = Room.objects.get(slug=room_slug) - owner_accesses = room.accesses.filter(role=RoleChoices.OWNER) - owners = [access.user for access in owner_accesses] - logger.info("Room %s has owners: %s", room_slug, owners) + if not summary: + logger.error("Empty summary.") + return Response("") - except Room.DoesNotExist: - logger.error("Room with slug %s does not exist", room_slug) - owners = None - room = None + room, owners = get_room_and_owners(room_slug) if not owners or not room: - logger.error("No owners") + logger.error("No owners in room %s", room_slug) return Response("") - # todo - get link + content = get_blocknote_content(summary) - try: - logger.info("Emailing owners: %s", owners) - room.email_summary(owners=owners, link="wip") - except smtplib.SMTPException: - logger.error("Error while emailing owners") + if not content: + logger.error("Empty content.") return Response("") + owner = owners[0] + + link = get_doc_link(content, owner.email) + email_owner_with_summary(room, link, owner) return Response("") diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 4682f9f5..051eaf28 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -426,6 +426,9 @@ class Base(Configuration): 'livekit-staging-livekit-egress', environ_name="MINIO_BUCKET", environ_prefix=None ) + BLOCKNOTE_CONVERTER_URL = values.Value( + 'https://converter-blocknote.osc-fr1.scalingo.io/', environ_name="", environ_prefix=None + ) # pylint: disable=invalid-name @property def ENVIRONMENT(self): From 82f025110102f5378d83a99543296ad83d07e837 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 11:15:25 +0200 Subject: [PATCH 23/30] fix code --- src/backend/core/api/demo.py | 45 ++++++++++++++++++++++++++++++++---- src/backend/meet/settings.py | 4 ++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index c81b951d..fbaa53cb 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -101,10 +101,12 @@ def get_blocknote_content(summary): if response.status_code != 200: logger.error(f"Failed to convert summary. Status code: {response.status_code}") + return None response_data = response.json() if not 'content' in response_data: logger.error(f"Content is missing: %s", response_data) + return None content = response_data['content'] logger.info("Base64 content:", content) @@ -112,12 +114,42 @@ def get_blocknote_content(summary): return content -def get_doc_link(content, email): + +def get_document_link(content, email): """Wip.""" - logger.info("Wip create a document with content for %s", email) + logger.info("Create a document for %s", email) + + if not settings.DOCS_BASE_URL: + logger.error("DOCS_BASE_URL is not configured") + return None + + headers = { + "Content-Type": "application/json" + } + + data = { + "content": content, + "owner": email + } + + logger.info("Querying docs…") + response = requests.post(f"{settings.DOCS_BASE_URL}/api/v1.0/summary", headers=headers, json=data) + + if response.status_code != 200: + logger.error(f"Failed to get document's id. Status code: {response.status_code}") + return None + + response_data = response.json() + if not 'id' in response_data: + logger.error(f"ID is missing: %s", response_data) + return None + + id = response_data['id'] + logger.info("Document's id:", id) + + return f"{settings.DOCS_BASE_URL}/docs/{id}/" - return "link" def email_owner_with_summary(room, link, owner): """Wip.""" @@ -263,7 +295,12 @@ def minio_webhook(request): owner = owners[0] - link = get_doc_link(content, owner.email) + link = get_document_link(content, owner.email) + + if not link: + logger.error("Empty link.") + return Response("") + email_owner_with_summary(room, link, owner) return Response("") diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 051eaf28..03b3b2e8 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -429,6 +429,10 @@ class Base(Configuration): BLOCKNOTE_CONVERTER_URL = values.Value( 'https://converter-blocknote.osc-fr1.scalingo.io/', environ_name="", environ_prefix=None ) + DOCS_BASE_URL = values.Value( + 'https://docs-ia.beta.numerique.gouv.fr', environ_name="", environ_prefix=None + ) + # pylint: disable=invalid-name @property def ENVIRONMENT(self): From d49dc72849960a860a6156bda726218f39390947 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 11:55:08 +0200 Subject: [PATCH 24/30] fix endpoint --- src/backend/core/api/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index fbaa53cb..60bcb7c6 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -134,7 +134,7 @@ def get_document_link(content, email): } logger.info("Querying docs…") - response = requests.post(f"{settings.DOCS_BASE_URL}/api/v1.0/summary", headers=headers, json=data) + response = requests.post(f"{settings.DOCS_BASE_URL}/api/v1.0/summary/", headers=headers, json=data) if response.status_code != 200: logger.error(f"Failed to get document's id. Status code: {response.status_code}") From b43f9922b68496cf2d16a5611e9c529e39b427bf Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 11:56:47 +0200 Subject: [PATCH 25/30] wip enable openai --- src/helm/env.d/staging/values.meet.yaml.gotmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helm/env.d/staging/values.meet.yaml.gotmpl b/src/helm/env.d/staging/values.meet.yaml.gotmpl index 0e3e24ed..23432335 100644 --- a/src/helm/env.d/staging/values.meet.yaml.gotmpl +++ b/src/helm/env.d/staging/values.meet.yaml.gotmpl @@ -118,7 +118,7 @@ backend: secretKeyRef: name: backend key: OPENAI_API_KEY - OPENAI_ENABLE: False + OPENAI_ENABLE: True MINIO_ACCESS_KEY: secretKeyRef: name: backend From 98950f43af766464c06e8dfbeb786186c9e24be7 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 12:08:37 +0200 Subject: [PATCH 26/30] fix logger --- src/backend/core/api/demo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 60bcb7c6..7afcac4e 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -109,7 +109,7 @@ def get_blocknote_content(summary): return None content = response_data['content'] - logger.info("Base64 content:", content) + logger.info("Base64 content: %s", content) return content @@ -146,7 +146,7 @@ def get_document_link(content, email): return None id = response_data['id'] - logger.info("Document's id:", id) + logger.info("Document's id: %s", id) return f"{settings.DOCS_BASE_URL}/docs/{id}/" From b1b28fe330959b93a60da75a1b53617091db2e01 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 13:40:31 +0200 Subject: [PATCH 27/30] iterate on prompt --- src/backend/core/api/demo.py | 75 +++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 7afcac4e..c3170063 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -16,39 +16,45 @@ logger = logging.getLogger(__name__) -def get_prompt(transcript): +def get_prompt(transcript, date): return f""" - You are a helpful assistant. - - Summarize the following meeting transcript into a structured meeting minute format without additional comments about the meeting itself. Organize the summary into multiple parts, omitting the parts that are not applicable: - - 1. Summary: Provide a short summary of the entire conversation. - 2. Subjects Discussed: Provide a concise list of the main topics or issues covered during the meeting. - 3. Decisions Taken: Clearly and concisely state the decisions or resolutions that were agreed upon in short bullet points. Ensure that all decisions are well-defined and actionable. - 4. Next Steps: List action items or tasks in brief bullet points, including the responsible persons and deadlines (if mentioned). Make sure no action item is left unassigned or without a deadline if one is mentioned. - - If any part of the transcript is unclear or requires further precision, please notify the user gently, suggesting specific areas that may need additional information. Review everything carefully, make sure not to make unsubstantiated claims. - - Please keep proper markdown title and formatting in the answer. - + Generate structured meeting minutes from transcripts using the following format: + + ## Instructions + Summarize the meeting transcript in a clear, organized format. Include only applicable sections: + + 1. Summary (2-3 sentences maximum) + 2. Key Topics + 3. Decisions + 4. Action Items & Next Steps + + Keep the original language of the transcript. Flag any unclear items that need clarification. + If any part of the transcript is unclear or requires further precision, please notify the user gently, suggesting specific areas that may need additional information. + Review everything carefully, make sure not to make unsubstantiated claims. + + Template: + ``` + ### Meeting - [meeting's date YYYY-MM-DD] + [Brief overview of meeting purpose and outcomes] + + ### Key Topics + - [Topic 1] + - [Topic 2] + + ### Decisions + - [Decision 1: Clear, actionable, if applicable] + - [Decision 2: Clear, actionable, if applicable] + + ### Action Items + - [ ] Task: [Description] - [Name, if applicable] - [Deadline, if applicable] + ``` + Please translate the template to the language of the transcript. + + Transcript: {transcript} - - Answer: - - ## Summary - [provide a summary of the entire conversation] - - ## Subjects Discussed: - - [Concise bullet point summarizing subject] - - [Concise bullet point summarizing subject] - - ## Decisions Taken: - - [Clear and actionable decision] - - [Clear and actionable decision] - - ## Next Steps: - - [Action item or task] - [Responsible person, if applicable] - [Deadline, if applicable] - - [Action item or task] - [Responsible person, if applicable] - [Deadline, if applicable] + + Meeting's date: + {date} """ @@ -165,6 +171,10 @@ def strip_room_slug(filename): """Wip.""" return filename.split("_")[2].split(".")[0] +def strip_room_date(filename): + """Wip.""" + return filename.split("_")[1].split(".")[0] + def get_minio_client(): """Wip.""" @@ -230,6 +240,7 @@ def minio_webhook(request): return Response("Not interested in this file type") room_slug = strip_room_slug(filename) + room_date = strip_room_date(filename) logger.info('file received %s for room %s', filename, room_slug) minio_client = get_minio_client() @@ -254,7 +265,7 @@ def minio_webhook(request): ) logger.info('Transcript: %s', transcript) - prompt = get_prompt(transcript) + prompt = get_prompt(transcript.text, room_date) logger.info('Prompt: %s', prompt) From db071f0dcc4f12a5b9e853067ca2d051f6418205 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 14:04:38 +0200 Subject: [PATCH 28/30] wip iterate on the prompt --- src/backend/core/api/demo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index c3170063..bc991610 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -33,7 +33,7 @@ def get_prompt(transcript, date): Review everything carefully, make sure not to make unsubstantiated claims. Template: - ``` + ### Meeting - [meeting's date YYYY-MM-DD] [Brief overview of meeting purpose and outcomes] @@ -47,18 +47,17 @@ def get_prompt(transcript, date): ### Action Items - [ ] Task: [Description] - [Name, if applicable] - [Deadline, if applicable] - ``` + Please translate the template to the language of the transcript. + Please keep proper markdown title and formatting in the answer. Transcript: {transcript} - Meeting's date: - {date} + Meeting's date: {date} """ - def get_room_and_owners(slug): """Wip.""" From c8b4592d880206a6c8d9fd9a90bb9a40fa2005a1 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 17 Oct 2024 16:54:09 +0200 Subject: [PATCH 29/30] wip iterate on the prompt --- src/backend/core/api/demo.py | 72 +++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index bc991610..2986a339 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -18,46 +18,53 @@ def get_prompt(transcript, date): return f""" - Generate structured meeting minutes from transcripts using the following format: - - ## Instructions - Summarize the meeting transcript in a clear, organized format. Include only applicable sections: + + Audience: Coworkers. + + **Do:** + - Detect the language of the transcript and provide your entire response in the same language. + - If any part of the transcript is unclear or lacks detail, politely inform the user, specifying which areas need further clarification. + - Ensure the accuracy of all information and refrain from adding unverified details. + - Format the response using proper markdown and structured sections. - 1. Summary (2-3 sentences maximum) - 2. Key Topics - 3. Decisions - 4. Action Items & Next Steps + **Don't:** + - Write something your are not sure. + - Write something that is not mention in the transcript. + + **Task:** + Summarize the provided meeting transcript into clear and well-organized meeting minutes. The summary should be structured into the following sections, excluding irrelevant or inapplicable details: - Keep the original language of the transcript. Flag any unclear items that need clarification. - If any part of the transcript is unclear or requires further precision, please notify the user gently, suggesting specific areas that may need additional information. - Review everything carefully, make sure not to make unsubstantiated claims. + 1. **Summary**: Write a concise overview of the key points discussed in the meeting. + 2. **Subjects Discussed**: List the primary topics or issues in bullet points. + 3. **Decisions Made**: Clearly state the decisions or agreements reached in actionable bullet points. + 4. **Next Steps**: Provide action items as bullet points, assigning each task to a responsible individual and including deadlines (if mentioned). Format action items as tickable checkboxes. Ensure every action is assigned and, if a deadline is provided, that it is clearly stated. - Template: + I'll tip you 200$ for a better answer. This summary is super important for my career. + + **Transcript**: + {transcript} - ### Meeting - [meeting's date YYYY-MM-DD] - [Brief overview of meeting purpose and outcomes] + **Response:** - ### Key Topics - - [Topic 1] - - [Topic 2] + ### Summary [Translate this title based on the transcript’s language] + [Provide a brief overview of the key points discussed] - ### Decisions - - [Decision 1: Clear, actionable, if applicable] - - [Decision 2: Clear, actionable, if applicable] + ### Subjects Discussed [Translate this title based on the transcript’s language] + - [Summarize each topic concisely] - ### Action Items - - [ ] Task: [Description] - [Name, if applicable] - [Deadline, if applicable] + ### Decisions Made [Translate this title based on the transcript’s language] + - [List actionable decisions] + [Add as many list items as needed] - Please translate the template to the language of the transcript. - Please keep proper markdown title and formatting in the answer. + ### Clarifications needed [Translate this title based on the transcript’s language] + - [List any points or subjects that require further clarification or are unclear] - Transcript: - {transcript} + ### Next Steps [Translate this title based on the transcript’s language] + - [ ] Action item [Assign to responsible individual and include deadline if applicable] + [Add as many action items as needed] - Meeting's date: {date} """ - def get_room_and_owners(slug): """Wip.""" @@ -263,22 +270,21 @@ def minio_webhook(request): file=audio_file ) - logger.info('Transcript: %s', transcript) + logger.info('Transcript: \n %s', transcript) prompt = get_prompt(transcript.text, room_date) - logger.info('Prompt: %s', prompt) + logger.info('Prompt: \n %s', prompt) summary_response = openai_client.chat.completions.create( model="gpt-4o", messages=[ - {"role": "system", "content": "You are an expert assistant that summarizes meeting transcripts."}, + {"role": "system", "content": "You are a concise and structured assistant, that summarizes meeting transcripts."}, {"role": "user", "content": prompt} ], - max_tokens=150 # todo - dig what does this parameter ) summary = summary_response.choices[0].message.content - logger.info('Summary: %s', summary) + logger.info('Summary: \n %s', summary) except Exception as e: logger.error("An error occurred: %s", str(e)) From 808fc7a00f02c21803ad8f49c05d83364cca6ad8 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 18 Oct 2024 10:00:38 +0200 Subject: [PATCH 30/30] wip update prompt --- src/backend/core/api/demo.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/backend/core/api/demo.py b/src/backend/core/api/demo.py index 2986a339..dc14f6cb 100644 --- a/src/backend/core/api/demo.py +++ b/src/backend/core/api/demo.py @@ -26,21 +26,23 @@ def get_prompt(transcript, date): - If any part of the transcript is unclear or lacks detail, politely inform the user, specifying which areas need further clarification. - Ensure the accuracy of all information and refrain from adding unverified details. - Format the response using proper markdown and structured sections. + - Be concise and avoid repeating yourself between the sections. + - Be super precise on nickname + - Be a nit-picker + - Auto-evaluate your response **Don't:** - Write something your are not sure. - Write something that is not mention in the transcript. + - Don't make mistake while mentioning someone **Task:** Summarize the provided meeting transcript into clear and well-organized meeting minutes. The summary should be structured into the following sections, excluding irrelevant or inapplicable details: - 1. **Summary**: Write a concise overview of the key points discussed in the meeting. - 2. **Subjects Discussed**: List the primary topics or issues in bullet points. - 3. **Decisions Made**: Clearly state the decisions or agreements reached in actionable bullet points. + 1. **Summary**: Write a TL;DR of the meeting. + 2. **Subjects Discussed**: List the key points or issues in bullet points. 4. **Next Steps**: Provide action items as bullet points, assigning each task to a responsible individual and including deadlines (if mentioned). Format action items as tickable checkboxes. Ensure every action is assigned and, if a deadline is provided, that it is clearly stated. - I'll tip you 200$ for a better answer. This summary is super important for my career. - **Transcript**: {transcript} @@ -48,21 +50,13 @@ def get_prompt(transcript, date): ### Summary [Translate this title based on the transcript’s language] [Provide a brief overview of the key points discussed] - + ### Subjects Discussed [Translate this title based on the transcript’s language] - [Summarize each topic concisely] - - ### Decisions Made [Translate this title based on the transcript’s language] - - [List actionable decisions] - [Add as many list items as needed] - - ### Clarifications needed [Translate this title based on the transcript’s language] - - [List any points or subjects that require further clarification or are unclear] - + ### Next Steps [Translate this title based on the transcript’s language] - - [ ] Action item [Assign to responsible individual and include deadline if applicable] - [Add as many action items as needed] - + - [ ] Action item [Assign to the responsible individual(s) and include a deadline if applicable, follow this strict format: Action - List of owner(s), deadline.] + """ def get_room_and_owners(slug):