Compare commits

...

2 Commits

Author SHA1 Message Date
Florent Chehab acd7c51758 (backend) compatible with summary v2 routes
* Track external process (transcribe) status through a webhook and
new Enum choices,
* Send v2 and v1 summary routes params so that both endpoint can still be used,
* Updated dev setup to make it work.
2026-05-26 18:28:08 +02:00
Florent Chehab 7e286fcb4b 💥(summary) removed summary v1 related code
We are in the process of moving meet to summary v2 routes and tasks.
This commits removes code from summary and moves some code from summary to meet,
which should have the responsability of this code.
2026-05-26 16:59:43 +02:00
37 changed files with 2555 additions and 880 deletions
-1
View File
@@ -305,7 +305,6 @@ jobs:
working-directory: src/summary working-directory: src/summary
env: env:
V1_TENANT_ID: 'test-tenant'
AUTHORIZED_TENANTS: '[{"id": "test-tenant", "api_key": "test-api-token", "webhook_url": "https://example.com/webhook", "webhook_api_key": "test-webhook-api-key"}]' AUTHORIZED_TENANTS: '[{"id": "test-tenant", "api_key": "test-api-token", "webhook_url": "https://example.com/webhook", "webhook_api_key": "test-webhook-api-key"}]'
AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage" AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage"
AWS_S3_ENDPOINT_URL: "minio:9000" AWS_S3_ENDPOINT_URL: "minio:9000"
+2 -1
View File
@@ -64,8 +64,9 @@ ALLOW_UNREGISTERED_ROOMS=False
RECORDING_ENABLE=True RECORDING_ENABLE=True
RECORDING_STORAGE_EVENT_ENABLE=True RECORDING_STORAGE_EVENT_ENABLE=True
RECORDING_STORAGE_EVENT_TOKEN=password RECORDING_STORAGE_EVENT_TOKEN=password
SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v1/tasks/ SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v2/async-jobs/transcribe/
SUMMARY_SERVICE_API_TOKEN=password SUMMARY_SERVICE_API_TOKEN=password
SUMMARY_SERVICE_WEBHOOK_API_TOKEN=webhook-password
RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording
# Recording encoding (LiveKit Egress advanced options). # Recording encoding (LiveKit Egress advanced options).
+45 -1
View File
@@ -39,7 +39,10 @@ from core import enums, models, utils
from core.api.filters import ListFileFilter from core.api.filters import ListFileFilter
from core.enums import MEDIA_STORAGE_URL_PATTERN from core.enums import MEDIA_STORAGE_URL_PATTERN
from core.recording.enums import FileExtension from core.recording.enums import FileExtension
from core.recording.event.authentication import StorageEventAuthentication from core.recording.event.authentication import (
RecordingProcessWebhookAuthentication,
StorageEventAuthentication,
)
from core.recording.event.exceptions import ( from core.recording.event.exceptions import (
InvalidBucketError, InvalidBucketError,
InvalidFilepathError, InvalidFilepathError,
@@ -995,6 +998,47 @@ class RecordingViewSet(
{"message": "Event processed."}, {"message": "Event processed."},
) )
@decorators.action(
detail=False,
methods=["post"],
url_path="external-process-hook",
authentication_classes=[RecordingProcessWebhookAuthentication],
)
def on_external_process_event_received(self, request, pk=None): # pylint: disable=unused-argument
"""Handle incoming external process events for recordings."""
logger.debug("Processing external process event %s", request.data)
data = request.data
if not data.get("job_id"):
raise drf_exceptions.ValidationError(detail="No job_id provided")
job_id = data["job_id"]
try:
recording = models.Recording.objects.get(external_process_id=job_id)
except models.Recording.DoesNotExist as e:
logger.warning("No recording found for job_id %s: %s", job_id, e)
recording = None
changed = False
if recording and data.get("type") == "transcript":
if data.get("status") == "success":
logger.info("External process received for recording %s", job_id)
recording.status = (
models.RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL
)
changed = True
if data.get("status") == "failure":
recording.status = models.RecordingStatusChoices.EXTERNAL_PROCESS_FAILED
changed = True
if changed and recording:
recording.save()
else:
logger.info("No changes to save for external process id %s", job_id)
return drf_response.Response(
{"message": "Event processed."},
)
def _auth_get_original_url(self, request): def _auth_get_original_url(self, request):
""" """
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header. Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
@@ -0,0 +1,23 @@
# Generated by Django 5.2.14 on 2026-05-26 14:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0018_rename_active_application_is_active'),
]
operations = [
migrations.AddField(
model_name='recording',
name='external_process_id',
field=models.CharField(blank=True, help_text='ID of the external process associated with the recording.', max_length=255, null=True, unique=True, verbose_name='External Process ID'),
),
migrations.AlterField(
model_name='recording',
name='status',
field=models.CharField(choices=[('initiated', 'Initiated'), ('active', 'Active'), ('stopped', 'Stopped'), ('saved', 'Saved'), ('aborted', 'Aborted'), ('failed_to_start', 'Failed to Start'), ('failed_to_stop', 'Failed to Stop'), ('notification_succeeded', 'Notification succeeded'), ('external_process_successful', 'External process successful'), ('external_process_failed', 'External process failed')], default='initiated', max_length=50),
),
]
+17
View File
@@ -60,6 +60,11 @@ class RecordingStatusChoices(models.TextChoices):
FAILED_TO_START = "failed_to_start", _("Failed to Start") FAILED_TO_START = "failed_to_start", _("Failed to Start")
FAILED_TO_STOP = "failed_to_stop", _("Failed to Stop") FAILED_TO_STOP = "failed_to_stop", _("Failed to Stop")
NOTIFICATION_SUCCEEDED = "notification_succeeded", _("Notification succeeded") NOTIFICATION_SUCCEEDED = "notification_succeeded", _("Notification succeeded")
EXTERNAL_PROCESS_SUCCESSFUL = (
"external_process_successful",
_("External process successful"),
)
EXTERNAL_PROCESS_FAILED = "external_process_failed", _("External process failed")
@classmethod @classmethod
def is_final(cls, status): def is_final(cls, status):
@@ -73,6 +78,8 @@ class RecordingStatusChoices(models.TextChoices):
cls.STOPPED, cls.STOPPED,
cls.SAVED, cls.SAVED,
cls.ABORTED, cls.ABORTED,
cls.EXTERNAL_PROCESS_SUCCESSFUL,
cls.EXTERNAL_PROCESS_FAILED,
cls.FAILED_TO_START, cls.FAILED_TO_START,
cls.FAILED_TO_STOP, cls.FAILED_TO_STOP,
} }
@@ -591,6 +598,14 @@ class Recording(BaseModel):
verbose_name=_("Recording options"), verbose_name=_("Recording options"),
help_text=_("Recording options"), help_text=_("Recording options"),
) )
external_process_id = models.CharField(
max_length=255,
null=True,
blank=True,
unique=True,
verbose_name=_("External Process ID"),
help_text=_("ID of the external process associated with the recording."),
)
class Meta: class Meta:
db_table = "meet_recording" db_table = "meet_recording"
@@ -646,6 +661,8 @@ class Recording(BaseModel):
return self.status in { return self.status in {
RecordingStatusChoices.NOTIFICATION_SUCCEEDED, RecordingStatusChoices.NOTIFICATION_SUCCEEDED,
RecordingStatusChoices.SAVED, RecordingStatusChoices.SAVED,
RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL,
RecordingStatusChoices.EXTERNAL_PROCESS_FAILED,
} }
@property @property
@@ -14,9 +14,9 @@ logger = logging.getLogger(__name__)
class MachineUser: class MachineUser:
"""Represent a non-interactive system user for automated storage operations.""" """Represent a non-interactive system user for automated storage operations."""
def __init__(self) -> None: def __init__(self, username: str = "storage_event_user") -> None:
self.pk = None self.pk = None
self.username = "storage_event_user" self.username = username
self.is_active = True self.is_active = True
@property @property
@@ -91,3 +91,29 @@ class StorageEventAuthentication(BaseAuthentication):
def authenticate_header(self, request): def authenticate_header(self, request):
"""Return the WWW-Authenticate header value.""" """Return the WWW-Authenticate header value."""
return f"{self.TOKEN_TYPE} realm='Storage event API'" return f"{self.TOKEN_TYPE} realm='Storage event API'"
class RecordingProcessWebhookAuthentication(BaseAuthentication):
"""
Custom authentication class for recording process webhook requests.
Validates the API key in the Authorization header.
"""
def authenticate(self, request):
"""
Authenticate the request and return a two-tuple of (user, token).
"""
logger.info("Authentificating")
authorization_header: str = request.headers.get("Authorization") or ""
if not secrets.compare_digest(
authorization_header.removeprefix("Bearer "),
settings.SUMMARY_SERVICE_WEBHOOK_API_TOKEN,
):
logger.warning(
"Authentication failed: Bad Authorization header (ip: %s)",
request.META.get("REMOTE_ADDR"),
)
raise AuthenticationFailed()
return MachineUser("external_process_user"), None
@@ -1,8 +1,10 @@
"""Service to notify external services when a new recording is ready.""" """Service to notify external services when a new recording is ready."""
import asyncio import asyncio
import json
import logging import logging
import smtplib import smtplib
import warnings
from datetime import datetime, timezone from datetime import datetime, timezone
from django.conf import settings from django.conf import settings
@@ -17,6 +19,7 @@ from asgiref.sync import async_to_sync
from livekit import api as livekit_api from livekit import api as livekit_api
from core import models, utils from core import models, utils
from core.utils import generate_download_s3_file_url
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -215,18 +218,45 @@ class NotificationService:
)(recording.worker_id) )(recording.worker_id)
payload = { payload = {
# Legacy V1 params to avoid a breaking change
"owner_id": str(owner_access.user.id), "owner_id": str(owner_access.user.id),
"recording_filename": recording.key, "recording_filename": recording.key,
"metadata_filename": metadata_filename, "metadata_filename": metadata_filename,
"email": owner_access.user.email, "email": owner_access.user.email,
"sub": owner_access.user.sub, "sub": owner_access.user.sub,
"room": recording.room.name, "room": recording.room.name,
"language": recording.options.get("language"),
"owner_timezone": str(owner_access.user.timezone), "owner_timezone": str(owner_access.user.timezone),
"download_link": f"{get_recording_download_base_url()}/{recording.id}", "download_link": f"{get_recording_download_base_url()}/{recording.id}",
"context_language": owner_access.user.language,
"recording_start_at": (started_at.isoformat() if started_at else None), "recording_start_at": (started_at.isoformat() if started_at else None),
"recording_end_at": (ended_at.isoformat() if ended_at else None), "recording_end_at": (ended_at.isoformat() if ended_at else None),
# V2 params
"user_sub": owner_access.user.sub,
"user_email": owner_access.user.email,
"cloud_storage_url": generate_download_s3_file_url(
recording.key, expires_in=60 * 60 * 24, override_domain=False
),
"language": recording.options.get("language", "fr"),
"context_language": owner_access.user.language,
"push_to_docs_config": {
"user_email": owner_access.user.email,
"title": "TODO",
"download_link": f"{get_recording_download_base_url()}/{recording.id}",
# For now the feature flag logic is handled on summary side
"auto_create_summary": True,
},
"metadata": (
{
"cloud_storage_url": generate_download_s3_file_url(
metadata_filename,
expires_in=60 * 60 * 24,
override_domain=False,
),
"started_at": started_at.isoformat(),
"ended_at": ended_at.isoformat(),
}
if (started_at and ended_at)
else None
),
} }
headers = { headers = {
@@ -242,6 +272,31 @@ class NotificationService:
timeout=30, timeout=30,
) )
response.raise_for_status() response.raise_for_status()
is_v2_implementation = False
try:
response_json = response.json()
# We do not require a job_id to avoid a breaking change
if job_id := response_json.get("job_id"):
recording.external_process_id = job_id
recording.save()
is_v2_implementation = True
except json.JSONDecodeError:
pass
if not is_v2_implementation:
warnings.warn(
"You are likely using your own implementation for the summary / "
"transcribe service."
"We have released a new version and API contract for that service, "
"we recommend checking it out"
# pylint: disable=line-too-long
"https://github.com/suitenumerique/meet/blob/19c2a378e7e66652afbfa3e749badd697e3917ac/src/summary/summary/api/route/tasks_v2.py "
"and mimicking it's behavior. We will remove the legacy "
"handling in a future version.",
DeprecationWarning,
stacklevel=2,
)
except requests.RequestException as exc: except requests.RequestException as exc:
logger.exception( logger.exception(
"Summary service error for recording %s. URL: %s. Exception: %s", "Summary service error for recording %s. URL: %s. Exception: %s",
@@ -0,0 +1,134 @@
"""
Test recordings API endpoints: external process hook.
"""
# pylint: disable=redefined-outer-name,unused-argument
import pytest
from ...factories import RecordingFactory
from ...models import RecordingStatusChoices
pytestmark = pytest.mark.django_db
@pytest.fixture
def external_process_settings(settings):
"""Configure authentication token for the external process webhook."""
settings.SUMMARY_SERVICE_WEBHOOK_API_TOKEN = "testWebhookToken"
return settings
def test_external_process_event_missing_authorization_header(
external_process_settings, client
):
"""Requests without authorization must be rejected."""
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"job_id": "job-1", "type": "transcript", "status": "success"},
)
assert response.status_code == 403
def test_external_process_event_wrong_bearer_token(external_process_settings, client):
"""Requests with invalid bearer token must be rejected."""
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"job_id": "job-1", "type": "transcript", "status": "success"},
HTTP_AUTHORIZATION="Bearer wrongToken",
)
assert response.status_code == 403
def test_external_process_event_missing_job_id(external_process_settings, client):
"""Payload without job_id must fail validation."""
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"type": "transcript", "status": "success"},
HTTP_AUTHORIZATION="Bearer testWebhookToken",
)
assert response.status_code == 400
assert "No job_id provided" in str(response.json())
def test_external_process_event_success_updates_recording_status(
external_process_settings, client
):
"""A successful transcript process should update recording status."""
recording = RecordingFactory(
status=RecordingStatusChoices.SAVED,
external_process_id="job-123",
)
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"job_id": "job-123", "type": "transcript", "status": "success"},
HTTP_AUTHORIZATION="Bearer testWebhookToken",
)
assert response.status_code == 200
assert response.json() == {"message": "Event processed."}
recording.refresh_from_db()
assert recording.status == RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL
def test_external_process_event_failure_updates_recording_status(
external_process_settings, client
):
"""A failing transcript process should update recording status."""
recording = RecordingFactory(
status=RecordingStatusChoices.SAVED,
external_process_id="job-456",
)
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"job_id": "job-456", "type": "transcript", "status": "failure"},
HTTP_AUTHORIZATION="Bearer testWebhookToken",
)
assert response.status_code == 200
assert response.json() == {"message": "Event processed."}
recording.refresh_from_db()
assert recording.status == RecordingStatusChoices.EXTERNAL_PROCESS_FAILED
def test_external_process_event_unknown_recording_is_ignored(
external_process_settings, client
):
"""Unknown job_id should not fail the webhook processing."""
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"job_id": "missing-job", "type": "transcript", "status": "success"},
HTTP_AUTHORIZATION="Bearer testWebhookToken",
)
assert response.status_code == 200
assert response.json() == {"message": "Event processed."}
def test_external_process_event_non_transcript_event_does_not_change_status(
external_process_settings, client
):
"""Only transcript events should update recording status."""
recording = RecordingFactory(
status=RecordingStatusChoices.SAVED,
external_process_id="job-789",
)
response = client.post(
"/api/v1.0/recordings/external-process-hook/",
{"job_id": "job-789", "type": "thumbnail", "status": "success"},
HTTP_AUTHORIZATION="Bearer testWebhookToken",
)
assert response.status_code == 200
assert response.json() == {"message": "Event processed."}
recording.refresh_from_db()
assert recording.status == RecordingStatusChoices.SAVED
+35
View File
@@ -455,3 +455,38 @@ def generate_upload_policy(file):
) )
return policy return policy
def generate_download_s3_file_url(
key, *, expires_in: int, override_domain: bool = True
):
"""
Generate a S3 signed download url for a given key.
"""
# This settings should be used if the backend application and the frontend application
# can't connect to the object storage with the same domain. This is the case in the
# docker compose stack used in development. The frontend application will use localhost
# to connect to the object storage while the backend application will use the object storage
# service name declared in the docker compose stack.
# This is needed because the domain name is used to compute the signature. So it can't be
# changed dynamically by the frontend application.
if settings.AWS_S3_DOMAIN_REPLACE and override_domain:
s3_client = boto3.client(
"s3",
aws_access_key_id=settings.AWS_S3_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_S3_SECRET_ACCESS_KEY,
endpoint_url=settings.AWS_S3_DOMAIN_REPLACE,
config=botocore.client.Config(
region_name=settings.AWS_S3_REGION_NAME,
signature_version=settings.AWS_S3_SIGNATURE_VERSION,
),
)
else:
s3_client = default_storage.connection.meta.client
return s3_client.generate_presigned_url(
ClientMethod="get_object",
Params={"Bucket": default_storage.bucket_name, "Key": key},
ExpiresIn=expires_in,
)
+100 -82
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-21 14:09+0000\n" "POT-Creation-Date: 2026-05-26 16:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -88,17 +88,17 @@ msgstr "Scopes"
msgid "Creator is me" msgid "Creator is me"
msgstr "Ersteller bin ich" msgstr "Ersteller bin ich"
#: core/api/serializers.py:88 #: core/api/serializers.py:89
msgid "You must be administrator or owner of a room to add accesses to it." msgid "You must be administrator or owner of a room to add accesses to it."
msgstr "" msgstr ""
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe " "Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
"hinzuzufügen." "hinzuzufügen."
#: core/api/serializers.py:509 #: core/api/serializers.py:534
msgid "This file extension is not allowed." msgid "This file extension is not allowed."
msgstr "Diese Dateiendung ist nicht erlaubt." msgstr "Diese Dateiendung ist nicht erlaubt."
#: core/api/viewsets.py:1090 #: core/api/viewsets.py:1228
msgid "You have reached the maximum number of files for this type." msgid "You have reached the maximum number of files for this type."
msgstr "Sie haben die maximale Anzahl an Dateien dieses Typs erreicht." msgstr "Sie haben die maximale Anzahl an Dateien dieses Typs erreicht."
@@ -146,51 +146,59 @@ msgstr "Stopp fehlgeschlagen"
msgid "Notification succeeded" msgid "Notification succeeded"
msgstr "Benachrichtigung erfolgreich" msgstr "Benachrichtigung erfolgreich"
#: core/models.py:89 #: core/models.py:65
msgid "External process successful"
msgstr ""
#: core/models.py:67
msgid "External process failed"
msgstr ""
#: core/models.py:96
msgid "SCREEN_RECORDING" msgid "SCREEN_RECORDING"
msgstr "BILDSCHIRMAUFZEICHNUNG" msgstr "BILDSCHIRMAUFZEICHNUNG"
#: core/models.py:90 #: core/models.py:97
msgid "TRANSCRIPT" msgid "TRANSCRIPT"
msgstr "TRANSKRIPT" msgstr "TRANSKRIPT"
#: core/models.py:96 #: core/models.py:103
msgid "Public Access" msgid "Public Access"
msgstr "Öffentlicher Zugriff" msgstr "Öffentlicher Zugriff"
#: core/models.py:97 #: core/models.py:104
msgid "Trusted Access" msgid "Trusted Access"
msgstr "Vertrauenswürdiger Zugriff" msgstr "Vertrauenswürdiger Zugriff"
#: core/models.py:98 #: core/models.py:105
msgid "Restricted Access" msgid "Restricted Access"
msgstr "Eingeschränkter Zugriff" msgstr "Eingeschränkter Zugriff"
#: core/models.py:110 #: core/models.py:117
msgid "id" msgid "id"
msgstr "ID" msgstr "ID"
#: core/models.py:111 #: core/models.py:118
msgid "primary key for the record as UUID" msgid "primary key for the record as UUID"
msgstr "Primärschlüssel des Eintrags als UUID" msgstr "Primärschlüssel des Eintrags als UUID"
#: core/models.py:117 #: core/models.py:124
msgid "created on" msgid "created on"
msgstr "erstellt am" msgstr "erstellt am"
#: core/models.py:118 #: core/models.py:125
msgid "date and time at which a record was created" msgid "date and time at which a record was created"
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags" msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
#: core/models.py:123 #: core/models.py:130
msgid "updated on" msgid "updated on"
msgstr "aktualisiert am" msgstr "aktualisiert am"
#: core/models.py:124 #: core/models.py:131
msgid "date and time at which a record was last updated" msgid "date and time at which a record was last updated"
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags" msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
#: core/models.py:144 #: core/models.py:151
msgid "" msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters." "_ characters."
@@ -198,11 +206,11 @@ msgstr ""
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen " "Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
"und die Zeichen @/./+/-/_ enthalten." "und die Zeichen @/./+/-/_ enthalten."
#: core/models.py:150 #: core/models.py:157
msgid "sub" msgid "sub"
msgstr "Sub" msgstr "Sub"
#: core/models.py:152 #: core/models.py:159
msgid "" msgid ""
"Optional for pending users; required upon account activation. 255 characters " "Optional for pending users; required upon account activation. 255 characters "
"or fewer. Letters, numbers, and @/./+/-/_ characters only." "or fewer. Letters, numbers, and @/./+/-/_ characters only."
@@ -210,55 +218,55 @@ msgstr ""
"Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. " "Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. "
"Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ Zeichen erlaubt." "Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ Zeichen erlaubt."
#: core/models.py:161 #: core/models.py:168
msgid "identity email address" msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse" msgstr "Identitäts-E-Mail-Adresse"
#: core/models.py:166 #: core/models.py:173
msgid "admin email address" msgid "admin email address"
msgstr "Administrator-E-Mail-Adresse" msgstr "Administrator-E-Mail-Adresse"
#: core/models.py:168 #: core/models.py:175
msgid "full name" msgid "full name"
msgstr "Vollständiger Name" msgstr "Vollständiger Name"
#: core/models.py:170 #: core/models.py:177
msgid "short name" msgid "short name"
msgstr "Kurzname" msgstr "Kurzname"
#: core/models.py:176 #: core/models.py:183
msgid "language" msgid "language"
msgstr "Sprache" msgstr "Sprache"
#: core/models.py:177 #: core/models.py:184
msgid "The language in which the user wants to see the interface." msgid "The language in which the user wants to see the interface."
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte." msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
#: core/models.py:183 #: core/models.py:190
msgid "The timezone in which the user wants to see times." msgid "The timezone in which the user wants to see times."
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte." msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
#: core/models.py:186 #: core/models.py:193
msgid "device" msgid "device"
msgstr "Gerät" msgstr "Gerät"
#: core/models.py:188 #: core/models.py:195
msgid "Whether the user is a device or a real user." msgid "Whether the user is a device or a real user."
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt." msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
#: core/models.py:191 #: core/models.py:198
msgid "staff status" msgid "staff status"
msgstr "Mitarbeiterstatus" msgstr "Mitarbeiterstatus"
#: core/models.py:193 #: core/models.py:200
msgid "Whether the user can log into this admin site." msgid "Whether the user can log into this admin site."
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann." msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
#: core/models.py:196 #: core/models.py:203
msgid "active" msgid "active"
msgstr "aktiv" msgstr "aktiv"
#: core/models.py:199 #: core/models.py:206
msgid "" msgid ""
"Whether this user should be treated as active. Unselect this instead of " "Whether this user should be treated as active. Unselect this instead of "
"deleting accounts." "deleting accounts."
@@ -266,66 +274,66 @@ msgstr ""
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies " "Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
"anstelle des Löschens des Kontos." "anstelle des Löschens des Kontos."
#: core/models.py:212 #: core/models.py:219
msgid "user" msgid "user"
msgstr "Benutzer" msgstr "Benutzer"
#: core/models.py:213 #: core/models.py:220
msgid "users" msgid "users"
msgstr "Benutzer" msgstr "Benutzer"
#: core/models.py:272 #: core/models.py:279
msgid "Resource" msgid "Resource"
msgstr "Ressource" msgstr "Ressource"
#: core/models.py:273 #: core/models.py:280
msgid "Resources" msgid "Resources"
msgstr "Ressourcen" msgstr "Ressourcen"
#: core/models.py:331 #: core/models.py:338
msgid "Resource access" msgid "Resource access"
msgstr "Ressourcenzugriff" msgstr "Ressourcenzugriff"
#: core/models.py:332 #: core/models.py:339
msgid "Resource accesses" msgid "Resource accesses"
msgstr "Ressourcenzugriffe" msgstr "Ressourcenzugriffe"
#: core/models.py:338 #: core/models.py:345
msgid "Resource access with this User and Resource already exists." msgid "Resource access with this User and Resource already exists."
msgstr "" msgstr ""
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert " "Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
"bereits." "bereits."
#: core/models.py:394 #: core/models.py:402
msgid "Visio room configuration" msgid "Visio room configuration"
msgstr "Visio-Raumkonfiguration" msgstr "Visio-Raumkonfiguration"
#: core/models.py:395 #: core/models.py:403
msgid "Values for Visio parameters to configure the room." msgid "Values for Visio parameters to configure the room."
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums." msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
#: core/models.py:402 #: core/models.py:410
msgid "Room PIN code" msgid "Room PIN code"
msgstr "PIN-Code für den Raum" msgstr "PIN-Code für den Raum"
#: core/models.py:403 #: core/models.py:411
msgid "Unique n-digit code that identifies this room in telephony mode." msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr "" msgstr ""
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert." "Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
#: core/models.py:409 core/models.py:563 #: core/models.py:417 core/models.py:571
msgid "Room" msgid "Room"
msgstr "Raum" msgstr "Raum"
#: core/models.py:410 #: core/models.py:418
msgid "Rooms" msgid "Rooms"
msgstr "Räume" msgstr "Räume"
#: core/models.py:574 #: core/models.py:582
msgid "Worker ID" msgid "Worker ID"
msgstr "Worker-ID" msgstr "Worker-ID"
#: core/models.py:576 #: core/models.py:584
msgid "" msgid ""
"Enter an identifier for the worker recording.This ID is retained even when " "Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking." "the worker stops, allowing for easy tracking."
@@ -334,141 +342,149 @@ msgstr ""
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen " "erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
"ermöglicht." "ermöglicht."
#: core/models.py:584 #: core/models.py:592
msgid "Recording mode" msgid "Recording mode"
msgstr "Aufzeichnungsmodus" msgstr "Aufzeichnungsmodus"
#: core/models.py:585 #: core/models.py:593
msgid "Defines the mode of recording being called." msgid "Defines the mode of recording being called."
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus." msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
#: core/models.py:590 core/models.py:591 #: core/models.py:598 core/models.py:599
msgid "Recording options" msgid "Recording options"
msgstr "Aufnahmeoptionen" msgstr "Aufnahmeoptionen"
#: core/models.py:597 #: core/models.py:606
msgid "External Process ID"
msgstr ""
#: core/models.py:607
msgid "ID of the external process associated with the recording."
msgstr ""
#: core/models.py:613
msgid "Recording" msgid "Recording"
msgstr "Aufzeichnung" msgstr "Aufzeichnung"
#: core/models.py:598 #: core/models.py:614
msgid "Recordings" msgid "Recordings"
msgstr "Aufzeichnungen" msgstr "Aufzeichnungen"
#: core/models.py:706 #: core/models.py:724
msgid "Recording/user relation" msgid "Recording/user relation"
msgstr "Beziehung Aufzeichnung/Benutzer" msgstr "Beziehung Aufzeichnung/Benutzer"
#: core/models.py:707 #: core/models.py:725
msgid "Recording/user relations" msgid "Recording/user relations"
msgstr "Beziehungen Aufzeichnung/Benutzer" msgstr "Beziehungen Aufzeichnung/Benutzer"
#: core/models.py:713 #: core/models.py:731
msgid "This user is already in this recording." msgid "This user is already in this recording."
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung." msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
#: core/models.py:719 #: core/models.py:737
msgid "This team is already in this recording." msgid "This team is already in this recording."
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung." msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
#: core/models.py:725 #: core/models.py:743
msgid "Either user or team must be set, not both." msgid "Either user or team must be set, not both."
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides." msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
#: core/models.py:742 #: core/models.py:760
msgid "Create rooms" msgid "Create rooms"
msgstr "Räume erstellen" msgstr "Räume erstellen"
#: core/models.py:743 #: core/models.py:761
msgid "List rooms" msgid "List rooms"
msgstr "Räume auflisten" msgstr "Räume auflisten"
#: core/models.py:744 #: core/models.py:762
msgid "Retrieve room details" msgid "Retrieve room details"
msgstr "Raumdetails abrufen" msgstr "Raumdetails abrufen"
#: core/models.py:745 #: core/models.py:763
msgid "Update rooms" msgid "Update rooms"
msgstr "Räume aktualisieren" msgstr "Räume aktualisieren"
#: core/models.py:746 #: core/models.py:764
msgid "Delete rooms" msgid "Delete rooms"
msgstr "Räume löschen" msgstr "Räume löschen"
#: core/models.py:759 #: core/models.py:777
msgid "Application name" msgid "Application name"
msgstr "Anwendungsname" msgstr "Anwendungsname"
#: core/models.py:760 #: core/models.py:778
msgid "Descriptive name for this application." msgid "Descriptive name for this application."
msgstr "Beschreibender Name für diese Anwendung." msgstr "Beschreibender Name für diese Anwendung."
#: core/models.py:770 #: core/models.py:788
msgid "Hashed on Save. Copy it now if this is a new secret." msgid "Hashed on Save. Copy it now if this is a new secret."
msgstr "" msgstr ""
"Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist." "Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
#: core/models.py:781 #: core/models.py:799
msgid "Application" msgid "Application"
msgstr "Anwendung" msgstr "Anwendung"
#: core/models.py:782 #: core/models.py:800
msgid "Applications" msgid "Applications"
msgstr "Anwendungen" msgstr "Anwendungen"
#: core/models.py:805 #: core/models.py:823
msgid "Enter a valid domain" msgid "Enter a valid domain"
msgstr "Geben Sie eine gültige Domain ein" msgstr "Geben Sie eine gültige Domain ein"
#: core/models.py:808 #: core/models.py:826
msgid "Domain" msgid "Domain"
msgstr "Domain" msgstr "Domain"
#: core/models.py:809 #: core/models.py:827
msgid "Email domain this application can act on behalf of." msgid "Email domain this application can act on behalf of."
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann." msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
#: core/models.py:821 #: core/models.py:839
msgid "Application domain" msgid "Application domain"
msgstr "Anwendungsdomain" msgstr "Anwendungsdomain"
#: core/models.py:822 #: core/models.py:840
msgid "Application domains" msgid "Application domains"
msgstr "Anwendungsdomains" msgstr "Anwendungsdomains"
#: core/models.py:840 #: core/models.py:858
msgid "Pending" msgid "Pending"
msgstr "Ausstehend" msgstr "Ausstehend"
#: core/models.py:848 #: core/models.py:866
msgid "Ready" msgid "Ready"
msgstr "Bereit" msgstr "Bereit"
#: core/models.py:854 #: core/models.py:872
msgid "Background image" msgid "Background image"
msgstr "Hintergrundbild" msgstr "Hintergrundbild"
#: core/models.py:866 #: core/models.py:884
msgid "title" msgid "title"
msgstr "Titel" msgstr "Titel"
#: core/models.py:890 #: core/models.py:908
msgid "Malware detection info when the analysis status is unsafe." msgid "Malware detection info when the analysis status is unsafe."
msgstr "" msgstr ""
"Informationen zur Malware-Erkennung, wenn der Analyse-Status unsicher ist." "Informationen zur Malware-Erkennung, wenn der Analyse-Status unsicher ist."
#: core/models.py:895 #: core/models.py:913
msgid "File" msgid "File"
msgstr "Datei" msgstr "Datei"
#: core/models.py:896 #: core/models.py:914
msgid "Files" msgid "Files"
msgstr "Dateien" msgstr "Dateien"
#: core/models.py:1000 #: core/models.py:1018
msgid "This file is already hard deleted." msgid "This file is already hard deleted."
msgstr "Diese Datei wurde bereits endgültig gelöscht." msgstr "Diese Datei wurde bereits endgültig gelöscht."
#: core/models.py:1010 #: core/models.py:1028
#, fuzzy #, fuzzy
#| msgid "To hard delete a file, it must first be soft deleted." #| msgid "To hard delete a file, it must first be soft deleted."
msgid "To hard delete a file, it must first be soft deleted." msgid "To hard delete a file, it must first be soft deleted."
@@ -476,7 +492,7 @@ msgstr ""
"Um eine Datei endgültig zu löschen, muss sie zuvor weich gelöscht worden " "Um eine Datei endgültig zu löschen, muss sie zuvor weich gelöscht worden "
"sein." "sein."
#: core/recording/event/notification.py:116 #: core/recording/event/notification.py:124
msgid "Your recording is ready" msgid "Your recording is ready"
msgstr "Ihre Aufzeichnung ist bereit" msgstr "Ihre Aufzeichnung ist bereit"
@@ -576,7 +592,9 @@ msgstr "So speichern Sie diese Aufzeichnung dauerhaft:"
#: core/templates/mail/html/screen_recording.html:208 #: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13 #: core/templates/mail/text/screen_recording.txt:13
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below " #, fuzzy
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
msgid "Click the \"Open\" button below "
msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten " msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten "
#: core/templates/mail/html/screen_recording.html:209 #: core/templates/mail/html/screen_recording.html:209
+100 -82
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-21 14:09+0000\n" "POT-Creation-Date: 2026-05-26 16:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -88,15 +88,15 @@ msgstr "Scopes"
msgid "Creator is me" msgid "Creator is me"
msgstr "Creator is me" msgstr "Creator is me"
#: core/api/serializers.py:88 #: core/api/serializers.py:89
msgid "You must be administrator or owner of a room to add accesses to it." msgid "You must be administrator or owner of a room to add accesses to it."
msgstr "You must be administrator or owner of a room to add accesses to it." msgstr "You must be administrator or owner of a room to add accesses to it."
#: core/api/serializers.py:509 #: core/api/serializers.py:534
msgid "This file extension is not allowed." msgid "This file extension is not allowed."
msgstr "This file extension is not allowed." msgstr "This file extension is not allowed."
#: core/api/viewsets.py:1090 #: core/api/viewsets.py:1228
msgid "You have reached the maximum number of files for this type." msgid "You have reached the maximum number of files for this type."
msgstr "You have reached the maximum number of files for this type." msgstr "You have reached the maximum number of files for this type."
@@ -144,51 +144,59 @@ msgstr "Failed to Stop"
msgid "Notification succeeded" msgid "Notification succeeded"
msgstr "Notification succeeded" msgstr "Notification succeeded"
#: core/models.py:89 #: core/models.py:65
msgid "External process successful"
msgstr ""
#: core/models.py:67
msgid "External process failed"
msgstr ""
#: core/models.py:96
msgid "SCREEN_RECORDING" msgid "SCREEN_RECORDING"
msgstr "SCREEN_RECORDING" msgstr "SCREEN_RECORDING"
#: core/models.py:90 #: core/models.py:97
msgid "TRANSCRIPT" msgid "TRANSCRIPT"
msgstr "TRANSCRIPT" msgstr "TRANSCRIPT"
#: core/models.py:96 #: core/models.py:103
msgid "Public Access" msgid "Public Access"
msgstr "Public Access" msgstr "Public Access"
#: core/models.py:97 #: core/models.py:104
msgid "Trusted Access" msgid "Trusted Access"
msgstr "Trusted Access" msgstr "Trusted Access"
#: core/models.py:98 #: core/models.py:105
msgid "Restricted Access" msgid "Restricted Access"
msgstr "Restricted Access" msgstr "Restricted Access"
#: core/models.py:110 #: core/models.py:117
msgid "id" msgid "id"
msgstr "id" msgstr "id"
#: core/models.py:111 #: core/models.py:118
msgid "primary key for the record as UUID" msgid "primary key for the record as UUID"
msgstr "primary key for the record as UUID" msgstr "primary key for the record as UUID"
#: core/models.py:117 #: core/models.py:124
msgid "created on" msgid "created on"
msgstr "created on" msgstr "created on"
#: core/models.py:118 #: core/models.py:125
msgid "date and time at which a record was created" msgid "date and time at which a record was created"
msgstr "date and time at which a record was created" msgstr "date and time at which a record was created"
#: core/models.py:123 #: core/models.py:130
msgid "updated on" msgid "updated on"
msgstr "updated on" msgstr "updated on"
#: core/models.py:124 #: core/models.py:131
msgid "date and time at which a record was last updated" msgid "date and time at which a record was last updated"
msgstr "date and time at which a record was last updated" msgstr "date and time at which a record was last updated"
#: core/models.py:144 #: core/models.py:151
msgid "" msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters." "_ characters."
@@ -196,11 +204,11 @@ msgstr ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters." "_ characters."
#: core/models.py:150 #: core/models.py:157
msgid "sub" msgid "sub"
msgstr "sub" msgstr "sub"
#: core/models.py:152 #: core/models.py:159
msgid "" msgid ""
"Optional for pending users; required upon account activation. 255 characters " "Optional for pending users; required upon account activation. 255 characters "
"or fewer. Letters, numbers, and @/./+/-/_ characters only." "or fewer. Letters, numbers, and @/./+/-/_ characters only."
@@ -208,55 +216,55 @@ msgstr ""
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ " "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
"characters only." "characters only."
#: core/models.py:161 #: core/models.py:168
msgid "identity email address" msgid "identity email address"
msgstr "identity email address" msgstr "identity email address"
#: core/models.py:166 #: core/models.py:173
msgid "admin email address" msgid "admin email address"
msgstr "admin email address" msgstr "admin email address"
#: core/models.py:168 #: core/models.py:175
msgid "full name" msgid "full name"
msgstr "full name" msgstr "full name"
#: core/models.py:170 #: core/models.py:177
msgid "short name" msgid "short name"
msgstr "short name" msgstr "short name"
#: core/models.py:176 #: core/models.py:183
msgid "language" msgid "language"
msgstr "language" msgstr "language"
#: core/models.py:177 #: core/models.py:184
msgid "The language in which the user wants to see the interface." msgid "The language in which the user wants to see the interface."
msgstr "The language in which the user wants to see the interface." msgstr "The language in which the user wants to see the interface."
#: core/models.py:183 #: core/models.py:190
msgid "The timezone in which the user wants to see times." msgid "The timezone in which the user wants to see times."
msgstr "The timezone in which the user wants to see times." msgstr "The timezone in which the user wants to see times."
#: core/models.py:186 #: core/models.py:193
msgid "device" msgid "device"
msgstr "device" msgstr "device"
#: core/models.py:188 #: core/models.py:195
msgid "Whether the user is a device or a real user." msgid "Whether the user is a device or a real user."
msgstr "Whether the user is a device or a real user." msgstr "Whether the user is a device or a real user."
#: core/models.py:191 #: core/models.py:198
msgid "staff status" msgid "staff status"
msgstr "staff status" msgstr "staff status"
#: core/models.py:193 #: core/models.py:200
msgid "Whether the user can log into this admin site." msgid "Whether the user can log into this admin site."
msgstr "Whether the user can log into this admin site." msgstr "Whether the user can log into this admin site."
#: core/models.py:196 #: core/models.py:203
msgid "active" msgid "active"
msgstr "active" msgstr "active"
#: core/models.py:199 #: core/models.py:206
msgid "" msgid ""
"Whether this user should be treated as active. Unselect this instead of " "Whether this user should be treated as active. Unselect this instead of "
"deleting accounts." "deleting accounts."
@@ -264,63 +272,63 @@ msgstr ""
"Whether this user should be treated as active. Unselect this instead of " "Whether this user should be treated as active. Unselect this instead of "
"deleting accounts." "deleting accounts."
#: core/models.py:212 #: core/models.py:219
msgid "user" msgid "user"
msgstr "user" msgstr "user"
#: core/models.py:213 #: core/models.py:220
msgid "users" msgid "users"
msgstr "users" msgstr "users"
#: core/models.py:272 #: core/models.py:279
msgid "Resource" msgid "Resource"
msgstr "Resource" msgstr "Resource"
#: core/models.py:273 #: core/models.py:280
msgid "Resources" msgid "Resources"
msgstr "Resources" msgstr "Resources"
#: core/models.py:331 #: core/models.py:338
msgid "Resource access" msgid "Resource access"
msgstr "Resource access" msgstr "Resource access"
#: core/models.py:332 #: core/models.py:339
msgid "Resource accesses" msgid "Resource accesses"
msgstr "Resource accesses" msgstr "Resource accesses"
#: core/models.py:338 #: core/models.py:345
msgid "Resource access with this User and Resource already exists." msgid "Resource access with this User and Resource already exists."
msgstr "Resource access with this User and Resource already exists." msgstr "Resource access with this User and Resource already exists."
#: core/models.py:394 #: core/models.py:402
msgid "Visio room configuration" msgid "Visio room configuration"
msgstr "Visio room configuration" msgstr "Visio room configuration"
#: core/models.py:395 #: core/models.py:403
msgid "Values for Visio parameters to configure the room." msgid "Values for Visio parameters to configure the room."
msgstr "Values for Visio parameters to configure the room." msgstr "Values for Visio parameters to configure the room."
#: core/models.py:402 #: core/models.py:410
msgid "Room PIN code" msgid "Room PIN code"
msgstr "Room PIN code" msgstr "Room PIN code"
#: core/models.py:403 #: core/models.py:411
msgid "Unique n-digit code that identifies this room in telephony mode." msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr "Unique n-digit code that identifies this room in telephony mode." msgstr "Unique n-digit code that identifies this room in telephony mode."
#: core/models.py:409 core/models.py:563 #: core/models.py:417 core/models.py:571
msgid "Room" msgid "Room"
msgstr "Room" msgstr "Room"
#: core/models.py:410 #: core/models.py:418
msgid "Rooms" msgid "Rooms"
msgstr "Rooms" msgstr "Rooms"
#: core/models.py:574 #: core/models.py:582
msgid "Worker ID" msgid "Worker ID"
msgstr "Worker ID" msgstr "Worker ID"
#: core/models.py:576 #: core/models.py:584
msgid "" msgid ""
"Enter an identifier for the worker recording.This ID is retained even when " "Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking." "the worker stops, allowing for easy tracking."
@@ -328,151 +336,159 @@ msgstr ""
"Enter an identifier for the worker recording.This ID is retained even when " "Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking." "the worker stops, allowing for easy tracking."
#: core/models.py:584 #: core/models.py:592
msgid "Recording mode" msgid "Recording mode"
msgstr "Recording mode" msgstr "Recording mode"
#: core/models.py:585 #: core/models.py:593
msgid "Defines the mode of recording being called." msgid "Defines the mode of recording being called."
msgstr "Defines the mode of recording being called." msgstr "Defines the mode of recording being called."
#: core/models.py:590 core/models.py:591 #: core/models.py:598 core/models.py:599
msgid "Recording options" msgid "Recording options"
msgstr "Recording options" msgstr "Recording options"
#: core/models.py:597 #: core/models.py:606
msgid "External Process ID"
msgstr ""
#: core/models.py:607
msgid "ID of the external process associated with the recording."
msgstr ""
#: core/models.py:613
msgid "Recording" msgid "Recording"
msgstr "Recording" msgstr "Recording"
#: core/models.py:598 #: core/models.py:614
msgid "Recordings" msgid "Recordings"
msgstr "Recordings" msgstr "Recordings"
#: core/models.py:706 #: core/models.py:724
msgid "Recording/user relation" msgid "Recording/user relation"
msgstr "Recording/user relation" msgstr "Recording/user relation"
#: core/models.py:707 #: core/models.py:725
msgid "Recording/user relations" msgid "Recording/user relations"
msgstr "Recording/user relations" msgstr "Recording/user relations"
#: core/models.py:713 #: core/models.py:731
msgid "This user is already in this recording." msgid "This user is already in this recording."
msgstr "This user is already in this recording." msgstr "This user is already in this recording."
#: core/models.py:719 #: core/models.py:737
msgid "This team is already in this recording." msgid "This team is already in this recording."
msgstr "This team is already in this recording." msgstr "This team is already in this recording."
#: core/models.py:725 #: core/models.py:743
msgid "Either user or team must be set, not both." msgid "Either user or team must be set, not both."
msgstr "Either user or team must be set, not both." msgstr "Either user or team must be set, not both."
#: core/models.py:742 #: core/models.py:760
#, fuzzy #, fuzzy
#| msgid "created on" #| msgid "created on"
msgid "Create rooms" msgid "Create rooms"
msgstr "Create rooms" msgstr "Create rooms"
#: core/models.py:743 #: core/models.py:761
msgid "List rooms" msgid "List rooms"
msgstr "List rooms" msgstr "List rooms"
#: core/models.py:744 #: core/models.py:762
msgid "Retrieve room details" msgid "Retrieve room details"
msgstr "Retrieve room details" msgstr "Retrieve room details"
#: core/models.py:745 #: core/models.py:763
#, fuzzy #, fuzzy
#| msgid "updated on" #| msgid "updated on"
msgid "Update rooms" msgid "Update rooms"
msgstr "Update rooms" msgstr "Update rooms"
#: core/models.py:746 #: core/models.py:764
msgid "Delete rooms" msgid "Delete rooms"
msgstr "Delete rooms" msgstr "Delete rooms"
#: core/models.py:759 #: core/models.py:777
msgid "Application name" msgid "Application name"
msgstr "Application name" msgstr "Application name"
#: core/models.py:760 #: core/models.py:778
msgid "Descriptive name for this application." msgid "Descriptive name for this application."
msgstr "Descriptive name for this application." msgstr "Descriptive name for this application."
#: core/models.py:770 #: core/models.py:788
msgid "Hashed on Save. Copy it now if this is a new secret." msgid "Hashed on Save. Copy it now if this is a new secret."
msgstr "Hashed on Save. Copy it now if this is a new secret." msgstr "Hashed on Save. Copy it now if this is a new secret."
#: core/models.py:781 #: core/models.py:799
msgid "Application" msgid "Application"
msgstr "Application" msgstr "Application"
#: core/models.py:782 #: core/models.py:800
msgid "Applications" msgid "Applications"
msgstr "Applications" msgstr "Applications"
#: core/models.py:805 #: core/models.py:823
msgid "Enter a valid domain" msgid "Enter a valid domain"
msgstr "Enter a valid domain" msgstr "Enter a valid domain"
#: core/models.py:808 #: core/models.py:826
msgid "Domain" msgid "Domain"
msgstr "Domain" msgstr "Domain"
#: core/models.py:809 #: core/models.py:827
msgid "Email domain this application can act on behalf of." msgid "Email domain this application can act on behalf of."
msgstr "Email domain this application can act on behalf of." msgstr "Email domain this application can act on behalf of."
#: core/models.py:821 #: core/models.py:839
msgid "Application domain" msgid "Application domain"
msgstr "Application domain" msgstr "Application domain"
#: core/models.py:822 #: core/models.py:840
msgid "Application domains" msgid "Application domains"
msgstr "Application domains" msgstr "Application domains"
#: core/models.py:840 #: core/models.py:858
#, fuzzy #, fuzzy
#| msgid "Recording" #| msgid "Recording"
msgid "Pending" msgid "Pending"
msgstr "Pending" msgstr "Pending"
#: core/models.py:848 #: core/models.py:866
msgid "Ready" msgid "Ready"
msgstr "Ready" msgstr "Ready"
#: core/models.py:854 #: core/models.py:872
msgid "Background image" msgid "Background image"
msgstr "Background image" msgstr "Background image"
#: core/models.py:866 #: core/models.py:884
msgid "title" msgid "title"
msgstr "title" msgstr "title"
#: core/models.py:890 #: core/models.py:908
msgid "Malware detection info when the analysis status is unsafe." msgid "Malware detection info when the analysis status is unsafe."
msgstr "Malware detection info when the analysis status is unsafe." msgstr "Malware detection info when the analysis status is unsafe."
#: core/models.py:895 #: core/models.py:913
msgid "File" msgid "File"
msgstr "File" msgstr "File"
#: core/models.py:896 #: core/models.py:914
msgid "Files" msgid "Files"
msgstr "Files" msgstr "Files"
#: core/models.py:1000 #: core/models.py:1018
#, fuzzy #, fuzzy
#| msgid "This user is already in this recording." #| msgid "This user is already in this recording."
msgid "This file is already hard deleted." msgid "This file is already hard deleted."
msgstr "This file is already hard deleted." msgstr "This file is already hard deleted."
#: core/models.py:1010 #: core/models.py:1028
msgid "To hard delete a file, it must first be soft deleted." msgid "To hard delete a file, it must first be soft deleted."
msgstr "To hard delete a file, it must first be soft deleted." msgstr "To hard delete a file, it must first be soft deleted."
#: core/recording/event/notification.py:116 #: core/recording/event/notification.py:124
msgid "Your recording is ready" msgid "Your recording is ready"
msgstr "Your recording is ready" msgstr "Your recording is ready"
@@ -572,7 +588,9 @@ msgstr "To keep this recording permanently:"
#: core/templates/mail/html/screen_recording.html:208 #: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13 #: core/templates/mail/text/screen_recording.txt:13
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below " #, fuzzy
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
msgid "Click the \"Open\" button below "
msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below " msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
#: core/templates/mail/html/screen_recording.html:209 #: core/templates/mail/html/screen_recording.html:209
+100 -82
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-21 14:09+0000\n" "POT-Creation-Date: 2026-05-26 16:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n" "Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -89,17 +89,17 @@ msgstr "Scopes"
msgid "Creator is me" msgid "Creator is me"
msgstr "Je suis le créateur" msgstr "Je suis le créateur"
#: core/api/serializers.py:88 #: core/api/serializers.py:89
msgid "You must be administrator or owner of a room to add accesses to it." msgid "You must be administrator or owner of a room to add accesses to it."
msgstr "" msgstr ""
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter " "Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
"des accès." "des accès."
#: core/api/serializers.py:509 #: core/api/serializers.py:534
msgid "This file extension is not allowed." msgid "This file extension is not allowed."
msgstr "Cette extension n'est pas autorisée" msgstr "Cette extension n'est pas autorisée"
#: core/api/viewsets.py:1090 #: core/api/viewsets.py:1228
msgid "You have reached the maximum number of files for this type." msgid "You have reached the maximum number of files for this type."
msgstr "Vous avez atteint le nombre maximum de fichiers de ce type" msgstr "Vous avez atteint le nombre maximum de fichiers de ce type"
@@ -147,53 +147,61 @@ msgstr "Échec à l'arrêt"
msgid "Notification succeeded" msgid "Notification succeeded"
msgstr "Notification réussie" msgstr "Notification réussie"
#: core/models.py:89 #: core/models.py:65
msgid "External process successful"
msgstr ""
#: core/models.py:67
msgid "External process failed"
msgstr ""
#: core/models.py:96
msgid "SCREEN_RECORDING" msgid "SCREEN_RECORDING"
msgstr "ENREGISTREMENT_ÉCRAN" msgstr "ENREGISTREMENT_ÉCRAN"
#: core/models.py:90 #: core/models.py:97
msgid "TRANSCRIPT" msgid "TRANSCRIPT"
msgstr "TRANSCRIPTION" msgstr "TRANSCRIPTION"
#: core/models.py:96 #: core/models.py:103
msgid "Public Access" msgid "Public Access"
msgstr "Accès public" msgstr "Accès public"
#: core/models.py:97 #: core/models.py:104
msgid "Trusted Access" msgid "Trusted Access"
msgstr "Accès de confiance" msgstr "Accès de confiance"
#: core/models.py:98 #: core/models.py:105
msgid "Restricted Access" msgid "Restricted Access"
msgstr "Accès restreint" msgstr "Accès restreint"
#: core/models.py:110 #: core/models.py:117
msgid "id" msgid "id"
msgstr "id" msgstr "id"
#: core/models.py:111 #: core/models.py:118
msgid "primary key for the record as UUID" msgid "primary key for the record as UUID"
msgstr "clé primaire pour l'enregistrement sous forme d'UUID" msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
#: core/models.py:117 #: core/models.py:124
msgid "created on" msgid "created on"
msgstr "créé le" msgstr "créé le"
#: core/models.py:118 #: core/models.py:125
msgid "date and time at which a record was created" msgid "date and time at which a record was created"
msgstr "date et heure auxquelles un enregistrement a été créé" msgstr "date et heure auxquelles un enregistrement a été créé"
#: core/models.py:123 #: core/models.py:130
msgid "updated on" msgid "updated on"
msgstr "mis à jour le" msgstr "mis à jour le"
#: core/models.py:124 #: core/models.py:131
msgid "date and time at which a record was last updated" msgid "date and time at which a record was last updated"
msgstr "" msgstr ""
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière " "date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
"fois" "fois"
#: core/models.py:144 #: core/models.py:151
msgid "" msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters." "_ characters."
@@ -201,11 +209,11 @@ msgstr ""
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des " "Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
"chiffres et les caractères @/./+/-/_." "chiffres et les caractères @/./+/-/_."
#: core/models.py:150 #: core/models.py:157
msgid "sub" msgid "sub"
msgstr "sub" msgstr "sub"
#: core/models.py:152 #: core/models.py:159
msgid "" msgid ""
"Optional for pending users; required upon account activation. 255 characters " "Optional for pending users; required upon account activation. 255 characters "
"or fewer. Letters, numbers, and @/./+/-/_ characters only." "or fewer. Letters, numbers, and @/./+/-/_ characters only."
@@ -213,55 +221,55 @@ msgstr ""
"Optionnel pour les utilisateurs en attente ; requis lors de l'activation du " "Optionnel pour les utilisateurs en attente ; requis lors de l'activation du "
"compte. 255 caractères maximum. Lettres, chiffres et @/./+/-/_ uniquement." "compte. 255 caractères maximum. Lettres, chiffres et @/./+/-/_ uniquement."
#: core/models.py:161 #: core/models.py:168
msgid "identity email address" msgid "identity email address"
msgstr "adresse e-mail d'identité" msgstr "adresse e-mail d'identité"
#: core/models.py:166 #: core/models.py:173
msgid "admin email address" msgid "admin email address"
msgstr "adresse e-mail d'administrateur" msgstr "adresse e-mail d'administrateur"
#: core/models.py:168 #: core/models.py:175
msgid "full name" msgid "full name"
msgstr "nom complet" msgstr "nom complet"
#: core/models.py:170 #: core/models.py:177
msgid "short name" msgid "short name"
msgstr "nom court" msgstr "nom court"
#: core/models.py:176 #: core/models.py:183
msgid "language" msgid "language"
msgstr "langue" msgstr "langue"
#: core/models.py:177 #: core/models.py:184
msgid "The language in which the user wants to see the interface." msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface." msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
#: core/models.py:183 #: core/models.py:190
msgid "The timezone in which the user wants to see times." msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures." msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
#: core/models.py:186 #: core/models.py:193
msgid "device" msgid "device"
msgstr "appareil" msgstr "appareil"
#: core/models.py:188 #: core/models.py:195
msgid "Whether the user is a device or a real user." msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel." msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
#: core/models.py:191 #: core/models.py:198
msgid "staff status" msgid "staff status"
msgstr "statut du personnel" msgstr "statut du personnel"
#: core/models.py:193 #: core/models.py:200
msgid "Whether the user can log into this admin site." msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration." msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
#: core/models.py:196 #: core/models.py:203
msgid "active" msgid "active"
msgstr "actif" msgstr "actif"
#: core/models.py:199 #: core/models.py:206
msgid "" msgid ""
"Whether this user should be treated as active. Unselect this instead of " "Whether this user should be treated as active. Unselect this instead of "
"deleting accounts." "deleting accounts."
@@ -269,65 +277,65 @@ msgstr ""
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option " "Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
"au lieu de supprimer des comptes." "au lieu de supprimer des comptes."
#: core/models.py:212 #: core/models.py:219
msgid "user" msgid "user"
msgstr "utilisateur" msgstr "utilisateur"
#: core/models.py:213 #: core/models.py:220
msgid "users" msgid "users"
msgstr "utilisateurs" msgstr "utilisateurs"
#: core/models.py:272 #: core/models.py:279
msgid "Resource" msgid "Resource"
msgstr "Ressource" msgstr "Ressource"
#: core/models.py:273 #: core/models.py:280
msgid "Resources" msgid "Resources"
msgstr "Ressources" msgstr "Ressources"
#: core/models.py:331 #: core/models.py:338
msgid "Resource access" msgid "Resource access"
msgstr "Accès aux ressources" msgstr "Accès aux ressources"
#: core/models.py:332 #: core/models.py:339
msgid "Resource accesses" msgid "Resource accesses"
msgstr "Accès aux ressources" msgstr "Accès aux ressources"
#: core/models.py:338 #: core/models.py:345
msgid "Resource access with this User and Resource already exists." msgid "Resource access with this User and Resource already exists."
msgstr "" msgstr ""
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà." "L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
#: core/models.py:394 #: core/models.py:402
msgid "Visio room configuration" msgid "Visio room configuration"
msgstr "Configuration de la salle de visioconférence" msgstr "Configuration de la salle de visioconférence"
#: core/models.py:395 #: core/models.py:403
msgid "Values for Visio parameters to configure the room." msgid "Values for Visio parameters to configure the room."
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle." msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
#: core/models.py:402 #: core/models.py:410
msgid "Room PIN code" msgid "Room PIN code"
msgstr "Code PIN de la salle" msgstr "Code PIN de la salle"
#: core/models.py:403 #: core/models.py:411
msgid "Unique n-digit code that identifies this room in telephony mode." msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr "" msgstr ""
"Code unique à n chiffres qui identifie cette salle en mode téléphonique." "Code unique à n chiffres qui identifie cette salle en mode téléphonique."
#: core/models.py:409 core/models.py:563 #: core/models.py:417 core/models.py:571
msgid "Room" msgid "Room"
msgstr "Salle" msgstr "Salle"
#: core/models.py:410 #: core/models.py:418
msgid "Rooms" msgid "Rooms"
msgstr "Salles" msgstr "Salles"
#: core/models.py:574 #: core/models.py:582
msgid "Worker ID" msgid "Worker ID"
msgstr "ID du Worker" msgstr "ID du Worker"
#: core/models.py:576 #: core/models.py:584
msgid "" msgid ""
"Enter an identifier for the worker recording.This ID is retained even when " "Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking." "the worker stops, allowing for easy tracking."
@@ -335,150 +343,158 @@ msgstr ""
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est " "Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
"conservé même lorsque le Worker s'arrête, permettant un suivi facile." "conservé même lorsque le Worker s'arrête, permettant un suivi facile."
#: core/models.py:584 #: core/models.py:592
msgid "Recording mode" msgid "Recording mode"
msgstr "Mode d'enregistrement" msgstr "Mode d'enregistrement"
#: core/models.py:585 #: core/models.py:593
msgid "Defines the mode of recording being called." msgid "Defines the mode of recording being called."
msgstr "Définit le mode d'enregistrement appelé." msgstr "Définit le mode d'enregistrement appelé."
#: core/models.py:590 core/models.py:591 #: core/models.py:598 core/models.py:599
msgid "Recording options" msgid "Recording options"
msgstr "Options d'enregistrement" msgstr "Options d'enregistrement"
#: core/models.py:597 #: core/models.py:606
msgid "External Process ID"
msgstr ""
#: core/models.py:607
msgid "ID of the external process associated with the recording."
msgstr ""
#: core/models.py:613
msgid "Recording" msgid "Recording"
msgstr "Enregistrement" msgstr "Enregistrement"
#: core/models.py:598 #: core/models.py:614
msgid "Recordings" msgid "Recordings"
msgstr "Enregistrements" msgstr "Enregistrements"
#: core/models.py:706 #: core/models.py:724
msgid "Recording/user relation" msgid "Recording/user relation"
msgstr "Relation enregistrement/utilisateur" msgstr "Relation enregistrement/utilisateur"
#: core/models.py:707 #: core/models.py:725
msgid "Recording/user relations" msgid "Recording/user relations"
msgstr "Relations enregistrement/utilisateur" msgstr "Relations enregistrement/utilisateur"
#: core/models.py:713 #: core/models.py:731
msgid "This user is already in this recording." msgid "This user is already in this recording."
msgstr "Cet utilisateur est déjà dans cet enregistrement." msgstr "Cet utilisateur est déjà dans cet enregistrement."
#: core/models.py:719 #: core/models.py:737
msgid "This team is already in this recording." msgid "This team is already in this recording."
msgstr "Cette équipe est déjà dans cet enregistrement." msgstr "Cette équipe est déjà dans cet enregistrement."
#: core/models.py:725 #: core/models.py:743
msgid "Either user or team must be set, not both." msgid "Either user or team must be set, not both."
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux." msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
#: core/models.py:742 #: core/models.py:760
msgid "Create rooms" msgid "Create rooms"
msgstr "Créer des salles" msgstr "Créer des salles"
#: core/models.py:743 #: core/models.py:761
msgid "List rooms" msgid "List rooms"
msgstr "Lister les salles" msgstr "Lister les salles"
#: core/models.py:744 #: core/models.py:762
msgid "Retrieve room details" msgid "Retrieve room details"
msgstr "Afficher les détails dune salle" msgstr "Afficher les détails dune salle"
#: core/models.py:745 #: core/models.py:763
msgid "Update rooms" msgid "Update rooms"
msgstr "Mettre à jour les salles" msgstr "Mettre à jour les salles"
#: core/models.py:746 #: core/models.py:764
msgid "Delete rooms" msgid "Delete rooms"
msgstr "Supprimer les salles" msgstr "Supprimer les salles"
#: core/models.py:759 #: core/models.py:777
msgid "Application name" msgid "Application name"
msgstr "Nom de lapplication" msgstr "Nom de lapplication"
#: core/models.py:760 #: core/models.py:778
msgid "Descriptive name for this application." msgid "Descriptive name for this application."
msgstr "Nom descriptif de cette application." msgstr "Nom descriptif de cette application."
#: core/models.py:770 #: core/models.py:788
msgid "Hashed on Save. Copy it now if this is a new secret." msgid "Hashed on Save. Copy it now if this is a new secret."
msgstr "" msgstr ""
"Haché lors de lenregistrement. Copiez-le maintenant sil sagit dun " "Haché lors de lenregistrement. Copiez-le maintenant sil sagit dun "
"nouveau secret." "nouveau secret."
#: core/models.py:781 #: core/models.py:799
msgid "Application" msgid "Application"
msgstr "Application" msgstr "Application"
#: core/models.py:782 #: core/models.py:800
msgid "Applications" msgid "Applications"
msgstr "Applications" msgstr "Applications"
#: core/models.py:805 #: core/models.py:823
msgid "Enter a valid domain" msgid "Enter a valid domain"
msgstr "Saisissez un domaine valide" msgstr "Saisissez un domaine valide"
#: core/models.py:808 #: core/models.py:826
msgid "Domain" msgid "Domain"
msgstr "Domaine" msgstr "Domaine"
#: core/models.py:809 #: core/models.py:827
msgid "Email domain this application can act on behalf of." msgid "Email domain this application can act on behalf of."
msgstr "Domaine de messagerie au nom duquel cette application peut agir." msgstr "Domaine de messagerie au nom duquel cette application peut agir."
#: core/models.py:821 #: core/models.py:839
msgid "Application domain" msgid "Application domain"
msgstr "Domaine dapplication" msgstr "Domaine dapplication"
#: core/models.py:822 #: core/models.py:840
msgid "Application domains" msgid "Application domains"
msgstr "Domaines dapplication" msgstr "Domaines dapplication"
#: core/models.py:840 #: core/models.py:858
msgid "Pending" msgid "Pending"
msgstr "En attente" msgstr "En attente"
#: core/models.py:848 #: core/models.py:866
msgid "Ready" msgid "Ready"
msgstr "Prêt" msgstr "Prêt"
#: core/models.py:854 #: core/models.py:872
msgid "Background image" msgid "Background image"
msgstr "Image de fond" msgstr "Image de fond"
#: core/models.py:866 #: core/models.py:884
msgid "title" msgid "title"
msgstr "Titre" msgstr "Titre"
#: core/models.py:890 #: core/models.py:908
msgid "Malware detection info when the analysis status is unsafe." msgid "Malware detection info when the analysis status is unsafe."
msgstr "" msgstr ""
"Information concernant la détection de Malware cand le statut n'est pas sain" "Information concernant la détection de Malware cand le statut n'est pas sain"
#: core/models.py:895 #: core/models.py:913
msgid "File" msgid "File"
msgstr "Fichier" msgstr "Fichier"
#: core/models.py:896 #: core/models.py:914
msgid "Files" msgid "Files"
msgstr "Fichiers" msgstr "Fichiers"
#: core/models.py:1000 #: core/models.py:1018
#, fuzzy #, fuzzy
#| msgid "This user is already in this recording." #| msgid "This user is already in this recording."
msgid "This file is already hard deleted." msgid "This file is already hard deleted."
msgstr "Ce fichier a été supprimé." msgstr "Ce fichier a été supprimé."
#: core/models.py:1010 #: core/models.py:1028
msgid "To hard delete a file, it must first be soft deleted." msgid "To hard delete a file, it must first be soft deleted."
msgstr "" msgstr ""
"Pour supprimer définitivement un fichier il doit d'abord avoir été marqué " "Pour supprimer définitivement un fichier il doit d'abord avoir été marqué "
"comme supprimé (soft delete)" "comme supprimé (soft delete)"
#: core/recording/event/notification.py:116 #: core/recording/event/notification.py:124
msgid "Your recording is ready" msgid "Your recording is ready"
msgstr "Votre enregistrement est prêt" msgstr "Votre enregistrement est prêt"
@@ -578,7 +594,9 @@ msgstr "Pour conserver cet enregistrement de façon permanente :"
#: core/templates/mail/html/screen_recording.html:208 #: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13 #: core/templates/mail/text/screen_recording.txt:13
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below " #, fuzzy
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
msgid "Click the \"Open\" button below "
msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous " msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous "
#: core/templates/mail/html/screen_recording.html:209 #: core/templates/mail/html/screen_recording.html:209
+100 -82
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-21 14:09+0000\n" "POT-Creation-Date: 2026-05-26 16:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -88,16 +88,16 @@ msgstr "Scopes"
msgid "Creator is me" msgid "Creator is me"
msgstr "Maker ben ik" msgstr "Maker ben ik"
#: core/api/serializers.py:88 #: core/api/serializers.py:89
msgid "You must be administrator or owner of a room to add accesses to it." msgid "You must be administrator or owner of a room to add accesses to it."
msgstr "" msgstr ""
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen." "Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
#: core/api/serializers.py:509 #: core/api/serializers.py:534
msgid "This file extension is not allowed." msgid "This file extension is not allowed."
msgstr "Deze bestandsextensie is niet toegestaan." msgstr "Deze bestandsextensie is niet toegestaan."
#: core/api/viewsets.py:1090 #: core/api/viewsets.py:1228
msgid "You have reached the maximum number of files for this type." msgid "You have reached the maximum number of files for this type."
msgstr "Het maximale aantal bestanden voor dit type is bereikt." msgstr "Het maximale aantal bestanden voor dit type is bereikt."
@@ -145,51 +145,59 @@ msgstr "Stoppen mislukt"
msgid "Notification succeeded" msgid "Notification succeeded"
msgstr "Notificatie geslaagd" msgstr "Notificatie geslaagd"
#: core/models.py:89 #: core/models.py:65
msgid "External process successful"
msgstr ""
#: core/models.py:67
msgid "External process failed"
msgstr ""
#: core/models.py:96
msgid "SCREEN_RECORDING" msgid "SCREEN_RECORDING"
msgstr "SCHERM_OPNAME" msgstr "SCHERM_OPNAME"
#: core/models.py:90 #: core/models.py:97
msgid "TRANSCRIPT" msgid "TRANSCRIPT"
msgstr "TRANSCRIPT" msgstr "TRANSCRIPT"
#: core/models.py:96 #: core/models.py:103
msgid "Public Access" msgid "Public Access"
msgstr "Openbare toegang" msgstr "Openbare toegang"
#: core/models.py:97 #: core/models.py:104
msgid "Trusted Access" msgid "Trusted Access"
msgstr "Vertrouwde toegang" msgstr "Vertrouwde toegang"
#: core/models.py:98 #: core/models.py:105
msgid "Restricted Access" msgid "Restricted Access"
msgstr "Beperkte toegang" msgstr "Beperkte toegang"
#: core/models.py:110 #: core/models.py:117
msgid "id" msgid "id"
msgstr "id" msgstr "id"
#: core/models.py:111 #: core/models.py:118
msgid "primary key for the record as UUID" msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor het record als UUID" msgstr "primaire sleutel voor het record als UUID"
#: core/models.py:117 #: core/models.py:124
msgid "created on" msgid "created on"
msgstr "aangemaakt op" msgstr "aangemaakt op"
#: core/models.py:118 #: core/models.py:125
msgid "date and time at which a record was created" msgid "date and time at which a record was created"
msgstr "datum en tijd waarop een record werd aangemaakt" msgstr "datum en tijd waarop een record werd aangemaakt"
#: core/models.py:123 #: core/models.py:130
msgid "updated on" msgid "updated on"
msgstr "bijgewerkt op" msgstr "bijgewerkt op"
#: core/models.py:124 #: core/models.py:131
msgid "date and time at which a record was last updated" msgid "date and time at which a record was last updated"
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt" msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
#: core/models.py:144 #: core/models.py:151
msgid "" msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters." "_ characters."
@@ -197,11 +205,11 @@ msgstr ""
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/" "Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
"_ tekens bevatten." "_ tekens bevatten."
#: core/models.py:150 #: core/models.py:157
msgid "sub" msgid "sub"
msgstr "sub" msgstr "sub"
#: core/models.py:152 #: core/models.py:159
msgid "" msgid ""
"Optional for pending users; required upon account activation. 255 characters " "Optional for pending users; required upon account activation. 255 characters "
"or fewer. Letters, numbers, and @/./+/-/_ characters only." "or fewer. Letters, numbers, and @/./+/-/_ characters only."
@@ -209,55 +217,55 @@ msgstr ""
"Optioneel voor gebruikers in afwachting; vereist bij accountactivering. " "Optioneel voor gebruikers in afwachting; vereist bij accountactivering. "
"Maximum 255 tekens. Alleen letters, cijfers en @/./+/-/_ toegestaan." "Maximum 255 tekens. Alleen letters, cijfers en @/./+/-/_ toegestaan."
#: core/models.py:161 #: core/models.py:168
msgid "identity email address" msgid "identity email address"
msgstr "identiteit e-mailadres" msgstr "identiteit e-mailadres"
#: core/models.py:166 #: core/models.py:173
msgid "admin email address" msgid "admin email address"
msgstr "beheerder e-mailadres" msgstr "beheerder e-mailadres"
#: core/models.py:168 #: core/models.py:175
msgid "full name" msgid "full name"
msgstr "volledige naam" msgstr "volledige naam"
#: core/models.py:170 #: core/models.py:177
msgid "short name" msgid "short name"
msgstr "korte naam" msgstr "korte naam"
#: core/models.py:176 #: core/models.py:183
msgid "language" msgid "language"
msgstr "taal" msgstr "taal"
#: core/models.py:177 #: core/models.py:184
msgid "The language in which the user wants to see the interface." msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wil zien." msgstr "De taal waarin de gebruiker de interface wil zien."
#: core/models.py:183 #: core/models.py:190
msgid "The timezone in which the user wants to see times." msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker tijden wil zien." msgstr "De tijdzone waarin de gebruiker tijden wil zien."
#: core/models.py:186 #: core/models.py:193
msgid "device" msgid "device"
msgstr "apparaat" msgstr "apparaat"
#: core/models.py:188 #: core/models.py:195
msgid "Whether the user is a device or a real user." msgid "Whether the user is a device or a real user."
msgstr "Of de gebruiker een apparaat is of een echte gebruiker." msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
#: core/models.py:191 #: core/models.py:198
msgid "staff status" msgid "staff status"
msgstr "personeelsstatus" msgstr "personeelsstatus"
#: core/models.py:193 #: core/models.py:200
msgid "Whether the user can log into this admin site." msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen op deze beheersite." msgstr "Of de gebruiker kan inloggen op deze beheersite."
#: core/models.py:196 #: core/models.py:203
msgid "active" msgid "active"
msgstr "actief" msgstr "actief"
#: core/models.py:199 #: core/models.py:206
msgid "" msgid ""
"Whether this user should be treated as active. Unselect this instead of " "Whether this user should be treated as active. Unselect this instead of "
"deleting accounts." "deleting accounts."
@@ -265,64 +273,64 @@ msgstr ""
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in " "Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
"plaats van accounts te verwijderen." "plaats van accounts te verwijderen."
#: core/models.py:212 #: core/models.py:219
msgid "user" msgid "user"
msgstr "gebruiker" msgstr "gebruiker"
#: core/models.py:213 #: core/models.py:220
msgid "users" msgid "users"
msgstr "gebruikers" msgstr "gebruikers"
#: core/models.py:272 #: core/models.py:279
msgid "Resource" msgid "Resource"
msgstr "Bron" msgstr "Bron"
#: core/models.py:273 #: core/models.py:280
msgid "Resources" msgid "Resources"
msgstr "Bronnen" msgstr "Bronnen"
#: core/models.py:331 #: core/models.py:338
msgid "Resource access" msgid "Resource access"
msgstr "Brontoegang" msgstr "Brontoegang"
#: core/models.py:332 #: core/models.py:339
msgid "Resource accesses" msgid "Resource accesses"
msgstr "Brontoegangsrechten" msgstr "Brontoegangsrechten"
#: core/models.py:338 #: core/models.py:345
msgid "Resource access with this User and Resource already exists." msgid "Resource access with this User and Resource already exists."
msgstr "Brontoegang met deze gebruiker en bron bestaat al." msgstr "Brontoegang met deze gebruiker en bron bestaat al."
#: core/models.py:394 #: core/models.py:402
msgid "Visio room configuration" msgid "Visio room configuration"
msgstr "Visio-ruimteconfiguratie" msgstr "Visio-ruimteconfiguratie"
#: core/models.py:395 #: core/models.py:403
msgid "Values for Visio parameters to configure the room." msgid "Values for Visio parameters to configure the room."
msgstr "Waarden voor Visio-parameters om de ruimte te configureren." msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
#: core/models.py:402 #: core/models.py:410
msgid "Room PIN code" msgid "Room PIN code"
msgstr "Pincode van de kamer" msgstr "Pincode van de kamer"
#: core/models.py:403 #: core/models.py:411
msgid "Unique n-digit code that identifies this room in telephony mode." msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr "" msgstr ""
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus." "Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
#: core/models.py:409 core/models.py:563 #: core/models.py:417 core/models.py:571
msgid "Room" msgid "Room"
msgstr "Ruimte" msgstr "Ruimte"
#: core/models.py:410 #: core/models.py:418
msgid "Rooms" msgid "Rooms"
msgstr "Ruimtes" msgstr "Ruimtes"
#: core/models.py:574 #: core/models.py:582
msgid "Worker ID" msgid "Worker ID"
msgstr "Worker ID" msgstr "Worker ID"
#: core/models.py:576 #: core/models.py:584
msgid "" msgid ""
"Enter an identifier for the worker recording.This ID is retained even when " "Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking." "the worker stops, allowing for easy tracking."
@@ -330,140 +338,148 @@ msgstr ""
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, " "Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is." "zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
#: core/models.py:584 #: core/models.py:592
msgid "Recording mode" msgid "Recording mode"
msgstr "Opnamemodus" msgstr "Opnamemodus"
#: core/models.py:585 #: core/models.py:593
msgid "Defines the mode of recording being called." msgid "Defines the mode of recording being called."
msgstr "Definieert de modus van opname die wordt aangeroepen." msgstr "Definieert de modus van opname die wordt aangeroepen."
#: core/models.py:590 core/models.py:591 #: core/models.py:598 core/models.py:599
msgid "Recording options" msgid "Recording options"
msgstr "Opnameopties" msgstr "Opnameopties"
#: core/models.py:597 #: core/models.py:606
msgid "External Process ID"
msgstr ""
#: core/models.py:607
msgid "ID of the external process associated with the recording."
msgstr ""
#: core/models.py:613
msgid "Recording" msgid "Recording"
msgstr "Opname" msgstr "Opname"
#: core/models.py:598 #: core/models.py:614
msgid "Recordings" msgid "Recordings"
msgstr "Opnames" msgstr "Opnames"
#: core/models.py:706 #: core/models.py:724
msgid "Recording/user relation" msgid "Recording/user relation"
msgstr "Opname/gebruiker-relatie" msgstr "Opname/gebruiker-relatie"
#: core/models.py:707 #: core/models.py:725
msgid "Recording/user relations" msgid "Recording/user relations"
msgstr "Opname/gebruiker-relaties" msgstr "Opname/gebruiker-relaties"
#: core/models.py:713 #: core/models.py:731
msgid "This user is already in this recording." msgid "This user is already in this recording."
msgstr "Deze gebruiker is al in deze opname." msgstr "Deze gebruiker is al in deze opname."
#: core/models.py:719 #: core/models.py:737
msgid "This team is already in this recording." msgid "This team is already in this recording."
msgstr "Dit team is al in deze opname." msgstr "Dit team is al in deze opname."
#: core/models.py:725 #: core/models.py:743
msgid "Either user or team must be set, not both." msgid "Either user or team must be set, not both."
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide." msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
#: core/models.py:742 #: core/models.py:760
msgid "Create rooms" msgid "Create rooms"
msgstr "Ruimtes aanmaken" msgstr "Ruimtes aanmaken"
#: core/models.py:743 #: core/models.py:761
msgid "List rooms" msgid "List rooms"
msgstr "Ruimtes weergeven" msgstr "Ruimtes weergeven"
#: core/models.py:744 #: core/models.py:762
msgid "Retrieve room details" msgid "Retrieve room details"
msgstr "Details van een ruimte ophalen" msgstr "Details van een ruimte ophalen"
#: core/models.py:745 #: core/models.py:763
msgid "Update rooms" msgid "Update rooms"
msgstr "Ruimtes bijwerken" msgstr "Ruimtes bijwerken"
#: core/models.py:746 #: core/models.py:764
msgid "Delete rooms" msgid "Delete rooms"
msgstr "Ruimtes verwijderen" msgstr "Ruimtes verwijderen"
#: core/models.py:759 #: core/models.py:777
msgid "Application name" msgid "Application name"
msgstr "Naam van de applicatie" msgstr "Naam van de applicatie"
#: core/models.py:760 #: core/models.py:778
msgid "Descriptive name for this application." msgid "Descriptive name for this application."
msgstr "Beschrijvende naam voor deze applicatie." msgstr "Beschrijvende naam voor deze applicatie."
#: core/models.py:770 #: core/models.py:788
msgid "Hashed on Save. Copy it now if this is a new secret." msgid "Hashed on Save. Copy it now if this is a new secret."
msgstr "" msgstr ""
"Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is." "Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
#: core/models.py:781 #: core/models.py:799
msgid "Application" msgid "Application"
msgstr "Applicatie" msgstr "Applicatie"
#: core/models.py:782 #: core/models.py:800
msgid "Applications" msgid "Applications"
msgstr "Applicaties" msgstr "Applicaties"
#: core/models.py:805 #: core/models.py:823
msgid "Enter a valid domain" msgid "Enter a valid domain"
msgstr "Voer een geldig domein in" msgstr "Voer een geldig domein in"
#: core/models.py:808 #: core/models.py:826
msgid "Domain" msgid "Domain"
msgstr "Domein" msgstr "Domein"
#: core/models.py:809 #: core/models.py:827
msgid "Email domain this application can act on behalf of." msgid "Email domain this application can act on behalf of."
msgstr "E-maildomein namens welke deze applicatie kan handelen." msgstr "E-maildomein namens welke deze applicatie kan handelen."
#: core/models.py:821 #: core/models.py:839
msgid "Application domain" msgid "Application domain"
msgstr "Applicatiedomein" msgstr "Applicatiedomein"
#: core/models.py:822 #: core/models.py:840
msgid "Application domains" msgid "Application domains"
msgstr "Applicatiedomeinen" msgstr "Applicatiedomeinen"
#: core/models.py:840 #: core/models.py:858
msgid "Pending" msgid "Pending"
msgstr "In afwachting" msgstr "In afwachting"
#: core/models.py:848 #: core/models.py:866
msgid "Ready" msgid "Ready"
msgstr "Klaar" msgstr "Klaar"
#: core/models.py:854 #: core/models.py:872
msgid "Background image" msgid "Background image"
msgstr "Achtergrondafbeelding" msgstr "Achtergrondafbeelding"
#: core/models.py:866 #: core/models.py:884
msgid "title" msgid "title"
msgstr "Titel" msgstr "Titel"
#: core/models.py:890 #: core/models.py:908
msgid "Malware detection info when the analysis status is unsafe." msgid "Malware detection info when the analysis status is unsafe."
msgstr "Informatie over malwaredetectie wanneer de analysestatus onveilig is." msgstr "Informatie over malwaredetectie wanneer de analysestatus onveilig is."
#: core/models.py:895 #: core/models.py:913
msgid "File" msgid "File"
msgstr "Bestand" msgstr "Bestand"
#: core/models.py:896 #: core/models.py:914
msgid "Files" msgid "Files"
msgstr "Bestanden" msgstr "Bestanden"
#: core/models.py:1000 #: core/models.py:1018
msgid "This file is already hard deleted." msgid "This file is already hard deleted."
msgstr "Dit bestand is al definitief verwijderd." msgstr "Dit bestand is al definitief verwijderd."
#: core/models.py:1010 #: core/models.py:1028
#, fuzzy #, fuzzy
#| msgid "To hard delete a file, it must first be soft deleted." #| msgid "To hard delete a file, it must first be soft deleted."
msgid "To hard delete a file, it must first be soft deleted." msgid "To hard delete a file, it must first be soft deleted."
@@ -471,7 +487,7 @@ msgstr ""
"Om een bestand definitief te verwijderen, moet het eerst zacht verwijderd " "Om een bestand definitief te verwijderen, moet het eerst zacht verwijderd "
"zijn." "zijn."
#: core/recording/event/notification.py:116 #: core/recording/event/notification.py:124
msgid "Your recording is ready" msgid "Your recording is ready"
msgstr "Je opname is klaar" msgstr "Je opname is klaar"
@@ -571,7 +587,9 @@ msgstr "Om deze opname permanent te bewaren:"
#: core/templates/mail/html/screen_recording.html:208 #: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13 #: core/templates/mail/text/screen_recording.txt:13
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below " #, fuzzy
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
msgid "Click the \"Open\" button below "
msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder " msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder "
#: core/templates/mail/html/screen_recording.html:209 #: core/templates/mail/html/screen_recording.html:209
+3
View File
@@ -744,6 +744,9 @@ class Base(Configuration):
SUMMARY_SERVICE_API_TOKEN = SecretFileValue( SUMMARY_SERVICE_API_TOKEN = SecretFileValue(
None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None
) )
SUMMARY_SERVICE_WEBHOOK_API_TOKEN = SecretFileValue(
None, environ_name="SUMMARY_SERVICE_WEBHOOK_API_TOKEN", environ_prefix=None
)
SCREEN_RECORDING_BASE_URL = values.Value( SCREEN_RECORDING_BASE_URL = values.Value(
None, environ_name="SCREEN_RECORDING_BASE_URL", environ_prefix=None None, environ_name="SCREEN_RECORDING_BASE_URL", environ_prefix=None
) )
@@ -105,8 +105,9 @@ backend:
RECORDING_ENABLE: True RECORDING_ENABLE: True
RECORDING_STORAGE_EVENT_ENABLE: True RECORDING_STORAGE_EVENT_ENABLE: True
RECORDING_STORAGE_EVENT_TOKEN: password RECORDING_STORAGE_EVENT_TOKEN: password
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/ SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/
SUMMARY_SERVICE_API_TOKEN: password SUMMARY_SERVICE_API_TOKEN: password
SUMMARY_SERVICE_WEBHOOK_API_TOKEN: webhook-password
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
ROOM_TELEPHONY_ENABLED: True ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem SSL_CERT_FILE: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
@@ -6,7 +6,26 @@ _summaryEnvVars: &summaryEnvVars
AWS_S3_ACCESS_KEY_ID: meet AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False AWS_S3_SECURE_ACCESS: False
AUTHORIZED_TENANTS: '[{"id": "dictaphone", "api_key": "dictaphone_token", "webhook_url": "http://dictaphone-backend.dictaphone.svc.cluster.local/api/v1.0/ai-jobs/webhook/", "webhook_api_key": "token_summary"}]' # Note: setting allowed_push_to_docs false because it won't work in dev mode
AUTHORIZED_TENANTS: >
[
{
"id": "dictaphone",
"api_key": "dictaphone_token",
"webhook_url": "http://dictaphone-backend.dictaphone.svc.cluster.local/api/v1.0/ai-jobs/webhook/",
"webhook_api_key": "token_summary",
"allowed_push_to_docs": false
},
{
"id": "visio",
"api_key": "password",
"webhook_url": "https://meet.127.0.0.1.nip.io/api/v1.0/recordings/external-process-hook/",
"webhook_api_key": "webhook-password",
"allowed_push_to_docs": false
}
]
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
IS_DOCS_INTEGRATION_ENABLED: false
WHISPERX_API_KEY: WHISPERX_API_KEY:
secretKeyRef: secretKeyRef:
name: secret-dev name: secret-dev
@@ -109,8 +128,9 @@ backend:
RECORDING_ENABLE: True RECORDING_ENABLE: True
RECORDING_STORAGE_EVENT_ENABLE: True RECORDING_STORAGE_EVENT_ENABLE: True
RECORDING_STORAGE_EVENT_TOKEN: password RECORDING_STORAGE_EVENT_TOKEN: password
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/ SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/
SUMMARY_SERVICE_API_TOKEN: password SUMMARY_SERVICE_API_TOKEN: password
SUMMARY_SERVICE_WEBHOOK_API_TOKEN: webhook-password
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
ROOM_TELEPHONY_ENABLED: True ROOM_TELEPHONY_ENABLED: True
ROOM_TELEPHONY_DEFAULT_COUNTRY: 'FR' ROOM_TELEPHONY_DEFAULT_COUNTRY: 'FR'
@@ -258,6 +278,23 @@ celerySummaryBackend:
- "-Q" - "-Q"
- "call-webhook-queue-v2" - "call-webhook-queue-v2"
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
extraVolumes:
- name: certs
configMap:
name: certifi
items:
- key: cacert.pem
path: cacert.pem
agentMetadata: agentMetadata:
replicas: 1 replicas: 1
envVars: envVars:
+2 -1
View File
@@ -14,7 +14,8 @@ dependencies = [
"posthog==7.9.12", "posthog==7.9.12",
"requests==2.33.0", "requests==2.33.0",
"sentry-sdk[fastapi, celery]==2.54.0", "sentry-sdk[fastapi, celery]==2.54.0",
"langfuse==4.0.0" "langfuse==4.0.0",
"ruff>=0.15.6",
] ]
[project.optional-dependencies] [project.optional-dependencies]
+1 -4
View File
@@ -2,11 +2,8 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from summary.api.route import tasks, tasks_v2 from summary.api.route import tasks_v2
from summary.core.security import verify_tenant_api_key from summary.core.security import verify_tenant_api_key
api_router_v1 = APIRouter(dependencies=[Depends(verify_tenant_api_key)])
api_router_v1.include_router(tasks.router_tasks_v1, tags=["tasks"])
api_router_v2 = APIRouter(dependencies=[Depends(verify_tenant_api_key)]) api_router_v2 = APIRouter(dependencies=[Depends(verify_tenant_api_key)])
api_router_v2.include_router(tasks_v2.router_tasks_v2, tags=["tasks"]) api_router_v2.include_router(tasks_v2.router_tasks_v2, tags=["tasks"])
-79
View File
@@ -1,79 +0,0 @@
"""API routes related to application tasks."""
import time
from typing import Optional
from celery.result import AsyncResult
from fastapi import APIRouter
from pydantic import BaseModel, field_validator
from summary.core.celery_worker import (
process_audio_transcribe_summarize_v2,
)
from summary.core.config import get_settings
settings = get_settings()
class TranscribeSummarizeTaskCreation(BaseModel):
"""Transcription and summarization parameters."""
owner_id: str
recording_filename: str
metadata_filename: Optional[str] = None
email: str
sub: str
version: Optional[int] = 2
room: Optional[str]
owner_timezone: Optional[str]
language: Optional[str]
download_link: Optional[str]
context_language: Optional[str] = None
recording_start_at: Optional[str] = None
recording_end_at: Optional[str] = None
@field_validator("language")
@classmethod
def validate_language(cls, v):
"""Validate 'language' parameter."""
if v is not None and v not in settings.whisperx_allowed_languages:
raise ValueError(
f"Language '{v}' is not allowed. "
f"Allowed languages: {', '.join(settings.whisperx_allowed_languages)}"
)
return v
router_tasks_v1 = APIRouter(prefix="/tasks")
@router_tasks_v1.post("/")
async def create_transcribe_summarize_task(request: TranscribeSummarizeTaskCreation):
"""Create a transcription and summarization task."""
task = process_audio_transcribe_summarize_v2.apply_async(
args=[
request.owner_id,
request.recording_filename,
request.metadata_filename,
request.email,
request.sub,
time.time(),
request.room,
request.owner_timezone,
request.language,
request.download_link,
request.context_language,
request.recording_start_at,
request.recording_end_at,
],
queue=settings.transcribe_queue,
)
return {"id": task.id, "message": "Task created"}
@router_tasks_v1.get("/{task_id}")
async def get_task_status(task_id: str):
"""Check task status by ID."""
task = AsyncResult(task_id)
return {"id": task_id, "status": task.status}
+18 -2
View File
@@ -1,7 +1,10 @@
"""API routes related to application tasks (V2 / tenant friendly).""" """API routes related to application tasks (V2 / tenant friendly)."""
import logging
from datetime import datetime, timezone
from celery.result import AsyncResult from celery.result import AsyncResult
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request, status
from summary.core.celery_worker import ( from summary.core.celery_worker import (
celery, celery,
@@ -20,6 +23,7 @@ from summary.core.shared_models import (
TranscribeWebhookSuccessPayload, TranscribeWebhookSuccessPayload,
) )
logger = logging.getLogger(__name__)
router_tasks_v2 = APIRouter() router_tasks_v2 = APIRouter()
@@ -29,8 +33,20 @@ async def create_transcribe_task_v2(
request_tenant: AuthorizedTenant = Depends(verify_tenant_api_key_v2), request_tenant: AuthorizedTenant = Depends(verify_tenant_api_key_v2),
): ):
"""Create a transcription task.""" """Create a transcription task."""
if (
request.push_to_docs_config is not None
and not request_tenant.allowed_push_to_docs
):
logger.error(
f"Push to docs is not allowed for this tenant ({request_tenant.id})."
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Push to docs is not allowed for this tenant.",
)
task = process_audio_transcribe_v2_task.apply_async( task = process_audio_transcribe_v2_task.apply_async(
args=[{**request.model_dump(), "tenant_id": request_tenant.id}] args=[{**request.model_dump(), "tenant_id": request_tenant.id, "received_at": datetime.now(timezone.utc)}]
) )
return TranscribeWebhookPendingPayload(job_id=task.id).model_dump() return TranscribeWebhookPendingPayload(job_id=task.id).model_dump()
+10 -8
View File
@@ -4,12 +4,14 @@ import json
import time import time
from collections import Counter from collections import Counter
from functools import lru_cache from functools import lru_cache
from urllib.parse import urlsplit, urlunsplit
import redis import redis
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from posthog import Posthog from posthog import Posthog
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.models import TranscribeTaskV2Payload
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
settings = get_settings() settings = get_settings()
@@ -107,23 +109,23 @@ class MetadataManager:
"""Check if task_id exists in tasks metadata cache.""" """Check if task_id exists in tasks metadata cache."""
return self._redis.exists(self._get_redis_key(task_id)) return self._redis.exists(self._get_redis_key(task_id))
def create(self, task_id, task_args): def create(self, task_id: str, task_payload: TranscribeTaskV2Payload):
"""Create initial metadata entry for a new task.""" """Create initial metadata entry for a new task."""
if self._is_disabled or self.has_task_id(task_id): if self._is_disabled or self.has_task_id(task_id):
return return
# Positional args mirror process_audio_transcribe_summarize_v2 signature:
# owner_id, recording_filename, metadata_filename, email, sub, received_at, ...
_, filename, _, email, _, received_at, *_ = task_args
start_time = time.time() start_time = time.time()
parts = urlsplit(task_payload.cloud_storage_url)
clean_url = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
initial_metadata = { initial_metadata = {
"start_time": start_time, "start_time": start_time,
"asr_model": settings.whisperx_asr_model, "asr_model": settings.whisperx_asr_model,
"retries": 0, "retries": 0,
"filename": filename, "filename": clean_url,
"email": email, "sub": task_payload.user_sub,
"queuing_time": round(start_time - received_at, 2), "email": task_payload.user_email,
"tenant_id": task_payload.tenant_id,
"queuing_time": round(start_time - task_payload.received_at.timestamp(), 2),
} }
self._save_metadata(task_id, initial_metadata) self._save_metadata(task_id, initial_metadata)
+100 -192
View File
@@ -4,7 +4,6 @@
import json import json
import time import time
from datetime import datetime
import openai import openai
import sentry_sdk import sentry_sdk
@@ -14,10 +13,12 @@ from requests import exceptions
from summary.core.analytics import MetadataManager, get_analytics from summary.core.analytics import MetadataManager, get_analytics
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.docs_service import create_document_in_docs
from summary.core.file_service import FileService, FileServiceException from summary.core.file_service import FileService, FileServiceException
from summary.core.llm_service import LLMException, LLMObservability, LLMService from summary.core.llm_service import LLMException, LLMObservability, LLMService
from summary.core.locales import get_locale from summary.core.locales import get_locale
from summary.core.models import ( from summary.core.models import (
RecordingMetadata,
SummarizeTaskV2Payload, SummarizeTaskV2Payload,
TranscribeTaskV2Payload, TranscribeTaskV2Payload,
) )
@@ -43,7 +44,6 @@ from summary.core.transcript_formatter import TranscriptFormatter
from summary.core.user_assign import resolve_speaker_identities from summary.core.user_assign import resolve_speaker_identities
from summary.core.webhook_service import ( from summary.core.webhook_service import (
call_webhook_v2, call_webhook_v2,
submit_content,
) )
settings = get_settings() settings = get_settings()
@@ -79,23 +79,17 @@ file_service = FileService()
def transcribe_audio( def transcribe_audio(
*, *,
task_id: str, task_id: str,
recording_filename: str | None = None,
language: str, language: str,
cloud_storage_url=None, cloud_storage_url: str,
raises: bool = False, raises: bool = False,
): ):
"""Transcribe an audio file using WhisperX. """Transcribe an audio file using WhisperX.
Downloads the audio from MinIO or a cloud storage URL, sends it to Downloads the audio from a cloud storage URL, sends it to
WhisperX for transcription, and tracks metadata throughout the process. WhisperX for transcription, and tracks metadata throughout the process.
Returns the transcription object, or None if the file could not be retrieved. Returns the transcription object, or None if the file could not be retrieved.
""" """
if bool(recording_filename) == bool(cloud_storage_url):
raise ValueError(
"Either filename or cloud_storage_url must be provided, but not both."
)
logger.info("Initiating WhisperX client") logger.info("Initiating WhisperX client")
whisperx_client = openai.OpenAI( whisperx_client = openai.OpenAI(
api_key=settings.whisperx_api_key.get_secret_value(), api_key=settings.whisperx_api_key.get_secret_value(),
@@ -106,7 +100,6 @@ def transcribe_audio(
# Transcription # Transcription
try: try:
with file_service.prepare_audio_file( with file_service.prepare_audio_file(
remote_object_key=recording_filename,
cloud_storage_url=cloud_storage_url, cloud_storage_url=cloud_storage_url,
) as (audio_file, metadata): ) as (audio_file, metadata):
metadata_manager.track(task_id, {"audio_length": metadata["duration"]}) metadata_manager.track(task_id, {"audio_length": metadata["duration"]})
@@ -149,11 +142,7 @@ def transcribe_audio(
cloud_storage_url.split("?", 1)[0] if cloud_storage_url else None cloud_storage_url.split("?", 1)[0] if cloud_storage_url else None
) )
logger.exception( logger.exception(
( ("Unexpected error while preparing file %s "),
"Unexpected error while preparing file | filename: %s "
"| cloud_storage_url: %s"
),
recording_filename,
redacted_cloud_storage_url, redacted_cloud_storage_url,
) )
return None return None
@@ -163,41 +152,31 @@ def transcribe_audio(
def resolve_speaker_identities_and_apply_to( def resolve_speaker_identities_and_apply_to(
transcription, recording_start_at, recording_end_at, metadata_filename, task_id *, transcription: WhisperXResponse, recording_metadata: RecordingMetadata, task_id
): ) -> WhisperXResponse:
"""Assign users to detected speakers and rewrite the transcriptions. """Assign users to detected speakers and rewrite the transcriptions.
Args: Args:
transcription: output of meet-whisperx after transcription and diarization transcription: output of meet-whisperx after transcription and diarization
recording_start_at: sourced from LiveKit FileInfo via the egress_ended webhook recording_metadata: Metadata of the recording
recording_end_at: sourced from LiveKit FileInfo via the egress_ended webhook
metadata_filename: name of metadata file containing VAD information in S3
task_id: current task id, for logging purposes task_id: current task id, for logging purposes
""" """
recording_start_dt = (
datetime.fromisoformat(recording_start_at) if recording_start_at else None
)
recording_end_dt = (
datetime.fromisoformat(recording_end_at) if recording_end_at else None
)
logger.debug( logger.debug(
"recording_start_dt: %s ; recording_end_dt: %s", "recording_start_dt: %s ; recording_end_dt: %s",
recording_start_dt, recording_metadata.started_at,
recording_end_dt, recording_metadata.ended_at,
) )
if (recording_start_dt is None) or (recording_end_dt is None):
logger.debug("Skipping resolve_speaker_identities")
return transcription
logger.debug("Running resolve_speaker_identities") logger.debug("Running resolve_speaker_identities")
try: try:
metadata = file_service.read_json(metadata_filename) metadata = file_service.read_cloud_storage_json(
recording_metadata.cloud_storage_url
)
speaker_mapping = resolve_speaker_identities( speaker_mapping = resolve_speaker_identities(
metadata, metadata,
transcription, transcription,
recording_start_dt, recording_metadata.started_at,
recording_end_dt, recording_metadata.ended_at,
) )
new_transcription = speaker_mapping.apply_to(transcription.model_dump()) new_transcription = speaker_mapping.apply_to(transcription.model_dump())
return new_transcription return new_transcription
@@ -225,11 +204,8 @@ def format_transcript(
transcription, transcription,
context_language: str | None, context_language: str | None,
language: str, language: str,
room: str | None,
recording_datetime: str | None,
owner_timezone: str | None,
download_link: str | None, download_link: str | None,
) -> tuple[str, str]: ) -> str:
"""Format a transcription into readable content with a title. """Format a transcription into readable content with a title.
Resolves the locale from context_language / language, then uses Resolves the locale from context_language / language, then uses
@@ -242,9 +218,6 @@ def format_transcript(
return formatter.format( return formatter.format(
transcription, transcription,
room=room,
recording_datetime=recording_datetime,
owner_timezone=owner_timezone,
download_link=download_link, download_link=download_link,
) )
@@ -267,130 +240,9 @@ def format_actions(llm_output: dict) -> str:
return "" return ""
@celery.task(
bind=True,
autoretry_for=[exceptions.HTTPError],
max_retries=settings.celery_max_retries,
queue=settings.transcribe_queue,
)
def process_audio_transcribe_summarize_v2(
self,
owner_id: str,
recording_filename: str,
metadata_filename: str | None,
email: str,
sub: str,
received_at: float,
room: str | None,
owner_timezone: str | None,
language: str | None,
download_link: str | None,
context_language: str | None = None,
recording_start_at: str | None = None,
recording_end_at: str | None = None,
):
"""Process an audio file by transcribing it and generating a summary.
This Celery task orchestrates:
1. Audio transcription via WhisperX
2. Transcript formatting
3. Webhook submission
4. Conditional summarization queuing
Args:
self: Celery task instance (passed on with bind=True)
owner_id: Unique identifier of the recording owner.
recording_filename: Name of the audio file in MinIO storage.
metadata_filename: Name of the audio file in MinIO storage.
email: Email address of the recording owner.
sub: OIDC subject identifier of the recording owner.
received_at: Unix timestamp when the recording was received.
room: room name where the recording took place.
owner_timezone: IANA timezone of the recording owner (e.g. "Europe/Paris").
language: ISO 639-1 language code for transcription.
download_link: URL to download the original recording.
context_language: ISO 639-1 language code of the meeting summary context text.
recording_start_at: ISO 8601 timestamp of when file recording actually started
(from LiveKit FileInfo.started_at via the egress_ended webhook).
recording_end_at: ISO 8601 timestamp of when file recording ended
(from LiveKit FileInfo.ended_at via the egress_ended webhook).
"""
logger.info(
"Notification received | Owner: %s | Room: %s",
owner_id,
room,
)
task_id = self.request.id
# Transcribe the audio
transcription = transcribe_audio(
task_id=task_id, recording_filename=recording_filename, language=language
)
if transcription is None:
return
# Assign speakers and rewrite transcription/diarization output
if settings.is_resolve_speaker_identities_enabled and (
metadata_filename is not None
):
transcription = resolve_speaker_identities_and_apply_to(
transcription,
recording_start_at,
recording_end_at,
metadata_filename,
task_id,
)
# Format output
content, title = format_transcript(
transcription,
context_language,
language,
room,
recording_start_at,
owner_timezone,
download_link,
)
submit_content(content, title, email, sub)
metadata_manager.capture(task_id, settings.posthog_event_success)
# LLM Summarization
if (
analytics.is_feature_enabled("summary-enabled", distinct_id=owner_id)
and settings.is_summary_enabled
):
logger.info("Queuing summary generation task.")
summarize_transcription.apply_async(
args=[owner_id, content, email, sub, title],
queue=settings.summarize_queue,
)
else:
logger.info("Summary generation not enabled for this user. Skipping.")
@signals.task_prerun.connect(sender=process_audio_transcribe_summarize_v2)
def task_started(task_id=None, task=None, args=None, **kwargs):
"""Signal handler called before task execution begins."""
task_args = args or []
metadata_manager.create(task_id, task_args)
@signals.task_retry.connect(sender=process_audio_transcribe_summarize_v2)
def task_retry_handler(request=None, reason=None, einfo=None, **kwargs):
"""Signal handler called when task execution retries."""
metadata_manager.retry(request.id)
@signals.task_failure.connect(sender=process_audio_transcribe_summarize_v2)
def task_failure_handler(task_id, exception=None, **kwargs):
"""Signal handler called when task execution fails permanently."""
metadata_manager.capture(task_id, settings.posthog_event_failure)
def summarize_transcription_internals( def summarize_transcription_internals(
*, owner_id: str, transcript: str, session_id: str *, user_sub: str, transcript: str, session_id: str
) -> str: ) -> str:
"""Generate a summary from the provided transcription text. """Generate a summary from the provided transcription text.
@@ -401,11 +253,11 @@ def summarize_transcription_internals(
""" """
logger.info( logger.info(
"Starting summarization task | Owner: %s", "Starting summarization task | Owner: %s",
owner_id, user_sub,
) )
user_has_tracing_consent = analytics.is_feature_enabled( user_has_tracing_consent = analytics.is_feature_enabled(
"summary-tracing-consent", distinct_id=owner_id "summary-tracing-consent", distinct_id=user_sub
) )
# NOTE: We must instantiate a new LLMObservability client for each task invocation # NOTE: We must instantiate a new LLMObservability client for each task invocation
@@ -416,7 +268,7 @@ def summarize_transcription_internals(
llm_observability = LLMObservability( llm_observability = LLMObservability(
user_has_tracing_consent=user_has_tracing_consent, user_has_tracing_consent=user_has_tracing_consent,
session_id=session_id, session_id=session_id,
user_id=owner_id, user_id=user_sub,
) )
llm_service = LLMService(llm_observability=llm_observability) llm_service = LLMService(llm_observability=llm_observability)
@@ -468,29 +320,6 @@ def summarize_transcription_internals(
return summary return summary
@celery.task(
bind=True,
autoretry_for=[LLMException, Exception],
max_retries=settings.celery_max_retries,
queue=settings.summarize_queue,
)
def summarize_transcription(
self, owner_id: str, transcript: str, email: str, sub: str, title: str
):
"""Generate a summary from the provided transcription text.
This Celery task performs the following operations:
1. Run summary internals
2. Sends the final summary via webhook.
"""
summary = summarize_transcription_internals(
owner_id=owner_id, transcript=transcript, session_id=self.request.id
)
summary_title = settings.summary_title_template.format(title=title)
submit_content(summary, summary_title, email, sub)
################################################################################## ##################################################################################
# Tasks v2 # Tasks v2
################################################################################## ##################################################################################
@@ -549,6 +378,64 @@ def process_audio_transcribe_v2_task(
).model_dump() ).model_dump()
) )
# Assign speakers and rewrite transcription/diarization output
if settings.is_resolve_speaker_identities_enabled and payload.metadata is not None:
try:
transcription_res = resolve_speaker_identities_and_apply_to(
transcription=transcription_res,
recording_metadata=payload.metadata,
task_id=job_id,
)
except BaseException as e:
logger.error(f"Failed to resolve speaker identities, skipping: {e}")
# We do it synchronously for now
if (
payload.push_to_docs_config
and settings.is_docs_integration_enabled
and settings.get_authorized_tenant(
tenant_id=payload.tenant_id
).allowed_push_to_docs
):
# Format output
content = format_transcript(
transcription_res,
payload.context_language,
payload.language,
payload.push_to_docs_config.download_link,
)
create_document_in_docs(
content=content,
title=payload.push_to_docs_config.title,
email=payload.push_to_docs_config.user_email,
sub=payload.user_sub,
)
if (
payload.push_to_docs_config.auto_create_summary
and analytics.is_feature_enabled(
"summary-enabled", distinct_id=payload.user_sub
)
and settings.is_summary_enabled
):
summary = summarize_transcription_internals(
user_sub=payload.user_sub,
transcript=content,
session_id=self.request.id,
)
locale = get_locale(payload.context_language, payload.language)
create_document_in_docs(
content=summary,
title=locale.summary_title_template.format(
title=payload.push_to_docs_config.title
),
email=payload.push_to_docs_config.user_email,
sub=payload.user_sub,
)
metadata_manager.capture(job_id, settings.posthog_event_success)
file_service.store_transcript( file_service.store_transcript(
transcript=transcription_res, transcript=transcription_res,
job_id=job_id, job_id=job_id,
@@ -561,9 +448,30 @@ def process_audio_transcribe_v2_task(
call_webhook_v2_task.apply_async( call_webhook_v2_task.apply_async(
args=[success_payload.model_dump(), payload.tenant_id] args=[success_payload.model_dump(), payload.tenant_id]
) )
metadata_manager.capture(job_id, settings.posthog_event_success)
return success_payload.model_dump() return success_payload.model_dump()
@signals.task_prerun.connect(sender=process_audio_transcribe_v2_task)
def task_started(task_id=None, task=None, args=None, **kwargs):
"""Signal handler called before task execution begins."""
if args:
metadata_manager.create(task_id, TranscribeTaskV2Payload.model_validate(args[0]))
@signals.task_retry.connect(sender=process_audio_transcribe_v2_task)
def task_retry_handler(request=None, reason=None, einfo=None, **kwargs):
"""Signal handler called when task execution retries."""
metadata_manager.retry(request.id)
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
def task_failure_handler(task_id, exception=None, **kwargs):
"""Signal handler called when task execution fails permanently."""
metadata_manager.capture(task_id, settings.posthog_event_failure)
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task) @signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
def handle_transcribe_v2_failed( def handle_transcribe_v2_failed(
sender, sender,
@@ -621,7 +529,7 @@ def summarize_v2_task(
""" """
payload = SummarizeTaskV2Payload.model_validate(payload) payload = SummarizeTaskV2Payload.model_validate(payload)
summary = summarize_transcription_internals( summary = summarize_transcription_internals(
owner_id=payload.user_sub, user_sub=payload.user_sub,
transcript=payload.content, transcript=payload.content,
session_id=self.request.id, session_id=self.request.id,
) )
+14 -52
View File
@@ -1,9 +1,8 @@
"""Application configuration and settings.""" """Application configuration and settings."""
import logging import logging
import os
from functools import cached_property, lru_cache from functools import cached_property, lru_cache
from typing import Annotated, Any, List, Literal, Mapping, Optional, Set from typing import Annotated, List, Mapping, Optional, Set
from fastapi import Depends from fastapi import Depends
from pydantic import ( from pydantic import (
@@ -34,9 +33,12 @@ class AuthorizedTenant(BaseModel):
title="Webhook API Key", title="Webhook API Key",
description="The api_key to authenticate the webhook request.", description="The api_key to authenticate the webhook request.",
) )
allowed_push_to_docs: bool = Field(
title="Allow Push to Docs",
V1_DEFAULT_TENANT_ID = "__deprecated_meet_tenant__" description="Whether to allow pushing transcript"
" and summaries to docs for this tenant.",
default=False,
)
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -45,14 +47,12 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", frozen=True) model_config = SettingsConfigDict(env_file=".env", frozen=True)
app_name: str = "summary" app_name: str = "summary"
app_api_v1_str: str = "/api/v1"
app_api_v2_str: str = "/api/v2" app_api_v2_str: str = "/api/v2"
# Authorized Tenants # Authorized Tenants
# Using env variables to store authorized tenants for now # Using env variables to store authorized tenants for now
# to avoid any other external dependency (DB) # to avoid any other external dependency (DB)
authorized_tenants: tuple[AuthorizedTenant, ...] = Field(default_factory=tuple) authorized_tenants: tuple[AuthorizedTenant, ...] = Field(default_factory=tuple)
v1_tenant_id: str = V1_DEFAULT_TENANT_ID
# Audio recordings # Audio recordings
recording_max_duration: Optional[int] = None recording_max_duration: Optional[int] = None
@@ -72,8 +72,6 @@ class Settings(BaseSettings):
celery_result_backend: str = "redis://redis/0" celery_result_backend: str = "redis://redis/0"
celery_max_retries: int = 1 celery_max_retries: int = 1
transcribe_queue: str = "transcribe-queue"
summarize_queue: str = "summarize-queue"
# v2 tasks # v2 tasks
transcribe_queue_v2: str = "transcribe-queue-v2" transcribe_queue_v2: str = "transcribe-queue-v2"
summarize_queue_v2: str = "summarize-queue-v2" summarize_queue_v2: str = "summarize-queue-v2"
@@ -114,15 +112,16 @@ class Settings(BaseSettings):
webhook_status_forcelist: List[int] = [502, 503, 504] webhook_status_forcelist: List[int] = [502, 503, 504]
webhook_backoff_factor: float = 0.1 webhook_backoff_factor: float = 0.1
# Locale
default_context_language: Literal["de", "en", "fr", "nl"] = "fr"
# Output related settings
summary_title_template: Optional[str] = "Résumé de {title}"
# Summary related settings # Summary related settings
is_summary_enabled: bool = True is_summary_enabled: bool = True
# Docs service configuration
is_docs_integration_enabled: bool = True
docs_base_url: str = "https://example.com"
docs_server_to_server_api_key: SecretStr = Field(
title="API key for using docs server to server api", default="NO_API_KEY"
)
# Sentry # Sentry
sentry_is_enabled: bool = False sentry_is_enabled: bool = False
sentry_dsn: Optional[str] = None sentry_dsn: Optional[str] = None
@@ -145,33 +144,6 @@ class Settings(BaseSettings):
task_tracker_redis_url: str = "redis://redis/0" task_tracker_redis_url: str = "redis://redis/0"
task_tracker_prefix: str = "task_metadata:" task_tracker_prefix: str = "task_metadata:"
@model_validator(mode="before")
@classmethod
def legacy_default_tenant_config(cls, data: Any) -> Any:
"""Migrate the legacy default tenant configuration."""
if isinstance(data, dict):
api_key = os.getenv("APP_API_TOKEN")
webhook_api_key = os.getenv("WEBHOOK_API_TOKEN")
webhook_url = os.getenv("WEBHOOK_URL")
if api_key and webhook_api_key and webhook_url:
logger.warning(
"Deprecated legacy app configuration detected, "
"please use only the new 'authorized_tenants' field instead."
)
authorized_tenants = list(data.get("authorized_tenants", []))
authorized_tenants.append(
AuthorizedTenant(
id=V1_DEFAULT_TENANT_ID,
api_key=SecretStr(api_key),
webhook_url=webhook_url,
webhook_api_key=SecretStr(webhook_api_key),
)
)
data["authorized_tenants"] = tuple(authorized_tenants)
return data
@model_validator(mode="after") @model_validator(mode="after")
def validate_authorized_tenants(self): def validate_authorized_tenants(self):
"""Validate authorized tenants configuration.""" """Validate authorized tenants configuration."""
@@ -190,16 +162,6 @@ class Settings(BaseSettings):
raise ValueError("Duplicate application API api_keys are not allowed") raise ValueError("Duplicate application API api_keys are not allowed")
return self return self
@model_validator(mode="after")
def validate_default_v1_tenant(self):
"""Validate default v1 tenant configuration."""
if not any(
tenant.id == self.v1_tenant_id for tenant in self.authorized_tenants
):
raise ValueError("v1 tenant is not configured in authorized tenants")
return self
@cached_property @cached_property
def authorized_tenant_api_keys(self) -> frozenset[str]: def authorized_tenant_api_keys(self) -> frozenset[str]:
"""Return a frozenset of authorized tenant API api_keys.""" """Return a frozenset of authorized tenant API api_keys."""
+78
View File
@@ -0,0 +1,78 @@
"""Service for delivering content to external destinations."""
import json
import logging
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
from summary.core.config import get_settings
settings = get_settings()
logger = logging.getLogger(__name__)
def _create_retry_session(api_key: str | None = None):
"""Create an HTTP session configured with retry logic."""
session = Session()
retries = Retry(
total=settings.webhook_max_retries,
backoff_factor=settings.webhook_backoff_factor,
status_forcelist=settings.webhook_status_forcelist,
allowed_methods={"POST"},
)
session.mount("https://", HTTPAdapter(max_retries=retries))
if api_key:
session.headers.update({"Authorization": f"Bearer {api_key}"})
return session
def _post_with_retries(*, url, data, api_key: str | None = None):
"""Send POST request with automatic retries."""
session = _create_retry_session(api_key=api_key)
try:
response = session.post(url, json=data, timeout=(20, 3 * 60))
response.raise_for_status()
return response
finally:
session.close()
def create_document_in_docs(*, content: str, title: str, email: str, sub: str) -> None:
"""Call the Docs API to create a document on behalf of the user there.
Builds the payload, sends it with retries, and logs the outcome.
"""
data = {
"title": title,
"content": content,
"email": email,
"sub": sub,
}
logger.debug("Submitting to %s", settings.docs_base_url)
logger.debug("Request payload: %s", json.dumps(data, indent=2))
response = _post_with_retries(
url=settings.docs_base_url,
api_key=settings.docs_server_to_server_api_key.get_secret_value(),
data=data,
)
try:
response_data = response.json()
document_id = response_data.get("id", "N/A")
except (json.JSONDecodeError, AttributeError):
document_id = "Unable to parse response"
response_data = response.text
logger.info(
"Delivery success | Document %s submitted (HTTP %s)",
document_id,
response.status_code,
)
logger.debug("Full response: %s", response_data)
+6 -88
View File
@@ -13,7 +13,6 @@ from urllib.parse import urlparse
import requests import requests
from minio import Minio from minio import Minio
from minio.error import MinioException, S3Error
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.shared_models import WhisperXResponse from summary.core.shared_models import WhisperXResponse
@@ -144,54 +143,6 @@ class FileService:
self._allowed_extensions = settings.recording_allowed_extensions self._allowed_extensions = settings.recording_allowed_extensions
self._max_duration = settings.recording_max_duration self._max_duration = settings.recording_max_duration
def _download_from_minio(self, remote_object_key) -> Path:
"""Download file from MinIO to local temporary file.
The file is downloaded to a temporary location for local manipulation
such as validation, conversion, or processing before being used.
"""
logger.info("Download recording | object_key: %s", remote_object_key)
if not remote_object_key:
logger.warning("Invalid object_key '%s'", remote_object_key)
raise ValueError("Invalid object_key")
extension = Path(remote_object_key).suffix.lower()
if extension not in self._allowed_extensions:
logger.warning("Invalid file extension '%s'", extension)
raise ValueError(f"Invalid file extension '{extension}'")
response = None
try:
response = self._minio_client.get_object(
self._bucket_name, remote_object_key
)
with tempfile.NamedTemporaryFile(
suffix=extension, delete=False, prefix="minio_download_"
) as tmp:
for chunk in response.stream(self._stream_chunk_size):
tmp.write(chunk)
tmp.flush()
local_path = Path(tmp.name)
logger.info("Recording successfully downloaded")
logger.debug("Recording local file path: %s", local_path)
return local_path
except (MinioException, S3Error) as e:
raise FileServiceException(
"Unexpected error while downloading object."
) from e
finally:
if response:
response.close()
def _download_from_cloud_storage_url(self, cloud_storage_url: str) -> Path: def _download_from_cloud_storage_url(self, cloud_storage_url: str) -> Path:
"""Download file from a cloud storage URL to local temporary file.""" """Download file from a cloud storage URL to local temporary file."""
logger.info( logger.info(
@@ -204,12 +155,6 @@ class FileService:
raise ValueError("Invalid cloud_storage_url") raise ValueError("Invalid cloud_storage_url")
extension = Path(urlparse(cloud_storage_url).path).suffix.lower() extension = Path(urlparse(cloud_storage_url).path).suffix.lower()
if extension not in self._allowed_extensions:
logger.warning(
"Invalid file extension '%s' from cloud_storage_url", extension
)
raise ValueError(f"Invalid file extension '{extension}'")
try: try:
with requests.get( with requests.get(
cloud_storage_url, cloud_storage_url,
@@ -302,33 +247,19 @@ class FileService:
os.remove(output_path) os.remove(output_path)
raise RuntimeError("Failed to extract audio.") from e raise RuntimeError("Failed to extract audio.") from e
def read_json(self, object_name: str) -> dict: def read_cloud_storage_json(self, cloud_storage_url: str) -> dict:
"""Read and parse a JSON file from MinIO storage.""" """Read and parse a JSON file from MinIO storage."""
logger.info("Reading JSON: %s", object_name) logger.info("Reading JSON: %s", cloud_storage_url)
local_path = self._download_from_cloud_storage_url(cloud_storage_url)
if not object_name:
raise ValueError("Invalid object_name")
response = None
try: try:
response = self._minio_client.get_object(self._bucket_name, object_name) return json.load(local_path.open("r"))
return json.loads(response.read())
except (MinioException, S3Error) as e:
raise FileServiceException(
"Unexpected error while reading JSON object."
) from e
except (json.JSONDecodeError, UnicodeDecodeError) as e: except (json.JSONDecodeError, UnicodeDecodeError) as e:
raise FileServiceException("Invalid JSON content.") from e raise FileServiceException("Invalid JSON content.") from e
finally:
if response:
response.close()
response.release_conn()
@contextmanager @contextmanager
def prepare_audio_file( def prepare_audio_file(
self, self,
remote_object_key: str | None = None, cloud_storage_url: str,
cloud_storage_url: str | None = None,
): ):
"""Download and prepare audio file for processing. """Download and prepare audio file for processing.
@@ -341,20 +272,7 @@ class FileService:
file_handle = None file_handle = None
try: try:
if bool(remote_object_key) == bool(cloud_storage_url): downloaded_path = self._download_from_cloud_storage_url(cloud_storage_url)
raise ValueError(
(
"Exactly one of 'remote_object_key' or "
"'cloud_storage_url' must be provided."
)
)
if cloud_storage_url:
downloaded_path = self._download_from_cloud_storage_url(
cloud_storage_url
)
else:
downloaded_path = self._download_from_minio(remote_object_key)
duration = self._validate_duration(downloaded_path) duration = self._validate_duration(downloaded_path)
+1
View File
@@ -30,4 +30,5 @@ Einige Punkte, die wir Ihnen empfehlen zu überprüfen:
document_title_template=( document_title_template=(
'Besprechung "{room}" am {room_recording_date} um {room_recording_time}' 'Besprechung "{room}" am {room_recording_date} um {room_recording_time}'
), ),
summary_title_template="Zusammenfassung von {title}",
) )
+1
View File
@@ -30,4 +30,5 @@ A few things we recommend you check:
document_title_template=( document_title_template=(
'Meeting "{room}" on {room_recording_date} at {room_recording_time}' 'Meeting "{room}" on {room_recording_date} at {room_recording_time}'
), ),
summary_title_template="Summary of {title}",
) )
+1
View File
@@ -30,4 +30,5 @@ Quelques points que nous vous conseillons de vérifier :
document_title_template=( document_title_template=(
'Réunion "{room}" du {room_recording_date} à {room_recording_time}' 'Réunion "{room}" du {room_recording_date} à {room_recording_time}'
), ),
summary_title_template="Résumé de {title}",
) )
+1
View File
@@ -30,4 +30,5 @@ Een paar punten die wij u aanraden te controleren:
document_title_template=( document_title_template=(
'Vergadering "{room}" op {room_recording_date} om {room_recording_time}' 'Vergadering "{room}" op {room_recording_date} om {room_recording_time}'
), ),
summary_title_template="Samenvatting van {title}",
) )
@@ -13,3 +13,4 @@ class LocaleStrings:
hallucination_replacement_text: str hallucination_replacement_text: str
document_default_title: str document_default_title: str
document_title_template: str document_title_template: str
summary_title_template: str
+56 -2
View File
@@ -1,6 +1,7 @@
"""Models for the API & Celery tasks creation.""" """Models for the API & Celery tasks creation."""
from datetime import datetime
from pydantic import BaseModel, Field, field_validator from pydantic import AwareDatetime, BaseModel, Field, field_validator
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.types import Url from summary.core.types import Url
@@ -12,6 +13,47 @@ class SharedV2TaskCreation(BaseModel):
"""Model that holds basic information for task creation.""" """Model that holds basic information for task creation."""
user_sub: str = Field(title="User Sub", description="The user's sub.") user_sub: str = Field(title="User Sub", description="The user's sub.")
user_email: str | None = Field(
title="User Email", description="The user's email for analytics purposes."
)
class RecordingMetadata(BaseModel):
"""Model for recording metadata."""
cloud_storage_url: Url = Field(
title="Cloud Storage URL",
description="The URL of the metadata file for speaker assignement.",
)
started_at: AwareDatetime = Field(title="Start time of the recording to transcribe")
ended_at: AwareDatetime = Field(title="End time of the recording to transcribe")
class PushToDocsBaseConfig(BaseModel):
"""Model containing information for pushing transcript and summaries to docs."""
user_email: str = Field(
title="User Email", description="The user's email, future owner of the docs."
)
title: str = Field(title="Title", description="The title for the created document.")
class PushToDocsTranscriptConfig(PushToDocsBaseConfig):
"""Model for push to docs information for transcripts."""
download_link: str | None = Field(
title="Download Link", description="The link to download the recording."
)
auto_create_summary: bool = Field(
title="Auto Create Summary Docs",
description="Whether to automatically create a summary "
"for the transcription task and push it to docs.",
default=False,
)
class PushToDocsSummaryConfig(PushToDocsBaseConfig):
"""Model for push to docs information for summaries."""
class TranscribeTaskV2Request(SharedV2TaskCreation): class TranscribeTaskV2Request(SharedV2TaskCreation):
@@ -27,7 +69,17 @@ class TranscribeTaskV2Request(SharedV2TaskCreation):
description="The language of the context text.", description="The language of the context text.",
) )
language: str = Field( language: str = Field(
title="Language", description="The language of the content to summarize." title="Language", description="The language of the content to transcribe."
)
metadata: RecordingMetadata | None = Field(
title="Metadata",
description="The metadata for the transcribe task.",
default=None,
)
push_to_docs_config: PushToDocsTranscriptConfig | None = Field(
title="Push to Docs info",
description="If set, configuration for pushing to docs",
default=None,
) )
@field_validator("language") @field_validator("language")
@@ -46,6 +98,7 @@ class TranscribeTaskV2Payload(TranscribeTaskV2Request):
"""Model for creating a transcribe and summarize task (used for actual task creation).""" # noqa: E501 """Model for creating a transcribe and summarize task (used for actual task creation).""" # noqa: E501
tenant_id: str = Field(title="Tenant ID", description="The ID of the tenant.") tenant_id: str = Field(title="Tenant ID", description="The ID of the tenant.")
received_at: datetime = Field(title="Received At", description="The time the task was received.")
class SummarizeTaskV2Request(SharedV2TaskCreation): class SummarizeTaskV2Request(SharedV2TaskCreation):
@@ -58,3 +111,4 @@ class SummarizeTaskV2Payload(SummarizeTaskV2Request):
"""Model for creating a summarize task (used for actual task creation).""" """Model for creating a summarize task (used for actual task creation)."""
tenant_id: str = Field(title="Tenant ID", description="The ID of the tenant.") tenant_id: str = Field(title="Tenant ID", description="The ID of the tenant.")
received_at: datetime = Field(title="Received At", description="The time the task was received.")
@@ -159,4 +159,5 @@ __all__ = [
"SummarizeWebhookPayloads", "SummarizeWebhookPayloads",
"WebhookPayloads", "WebhookPayloads",
"WhisperXResponse", "WhisperXResponse",
"webhook_payload_adapter",
] ]
@@ -1,9 +1,6 @@
"""Transcript formatting into readable conversation format with speaker labels.""" """Transcript formatting into readable conversation format with speaker labels."""
import logging import logging
from datetime import datetime
from typing import Tuple
from zoneinfo import ZoneInfo
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.locales import LocaleStrings from summary.core.locales import LocaleStrings
@@ -41,12 +38,9 @@ class TranscriptFormatter:
def format( def format(
self, self,
transcription, transcription,
room: str | None = None,
recording_datetime: str | None = None,
owner_timezone: str | None = None,
download_link: str | None = None, download_link: str | None = None,
) -> Tuple[str, str]: ) -> str:
"""Format transcription into the final document and its title.""" """Format transcription into the final document."""
segments = self._get_segments(transcription) segments = self._get_segments(transcription)
if not segments: if not segments:
@@ -56,9 +50,7 @@ class TranscriptFormatter:
content = self._remove_hallucinations(content) content = self._remove_hallucinations(content)
content = self._add_header(content, download_link) content = self._add_header(content, download_link)
title = self._generate_title(room, recording_datetime, owner_timezone) return content
return content, title
def _remove_hallucinations(self, content: str) -> str: def _remove_hallucinations(self, content: str) -> str:
"""Remove hallucination patterns from content.""" """Remove hallucination patterns from content."""
@@ -96,23 +88,3 @@ class TranscriptFormatter:
content = header + content content = header + content
return content return content
def _generate_title(
self,
room: str | None = None,
recording_datetime: str | None = None,
owner_timezone: str | None = None,
) -> str:
"""Generate title from context or return default."""
if not room or not recording_datetime:
return self._locale.document_default_title
dt = datetime.fromisoformat(recording_datetime)
if owner_timezone:
dt = dt.astimezone(ZoneInfo(owner_timezone))
return self._locale.document_title_template.format(
room=room,
room_recording_date=dt.strftime("%Y-%m-%d"),
room_recording_time=dt.strftime("%H:%M"),
)
@@ -4,9 +4,6 @@ import json
import logging import logging
import requests import requests
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
from summary.core.config import get_settings from summary.core.config import get_settings
from summary.core.shared_models import ( from summary.core.shared_models import (
@@ -18,83 +15,6 @@ settings = get_settings()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _create_retry_session(api_key: str | None = None):
"""Create an HTTP session configured with retry logic."""
session = Session()
retries = Retry(
total=settings.webhook_max_retries,
backoff_factor=settings.webhook_backoff_factor,
status_forcelist=settings.webhook_status_forcelist,
allowed_methods={"POST"},
)
session.mount("https://", HTTPAdapter(max_retries=retries))
if api_key:
session.headers.update({"Authorization": f"Bearer {api_key}"})
return session
def _post_with_retries(*, url, data, api_key: str | None = None):
"""Send POST request with automatic retries."""
session = _create_retry_session(api_key=api_key)
try:
response = session.post(url, json=data)
response.raise_for_status()
return response
finally:
session.close()
def call_webhook_v1(*, tenant_id: str, payload: dict) -> None:
"""Call webhook with payload a payload and optional token."""
tenant = settings.get_authorized_tenant(tenant_id=tenant_id)
logger.debug("Submitting to %s", tenant.webhook_url)
logger.debug("Request payload: %s", json.dumps(payload, indent=2))
response = _post_with_retries(
url=tenant.webhook_url,
api_key=tenant.webhook_api_key.get_secret_value(),
data=payload,
)
try:
response_data = response.json()
document_id = response_data.get("id", "N/A")
except (json.JSONDecodeError, AttributeError):
document_id = "Unable to parse response"
response_data = response.text
logger.info(
"Delivery success | Document %s submitted (HTTP %s)",
document_id,
response.status_code,
)
logger.debug("Full response: %s", response_data)
def submit_content(content: str, title: str, email: str, sub: str) -> None:
"""Submit content to the configured webhook destination.
Builds the payload, sends it with retries, and logs the outcome.
Notes:
Deprecated: Use call_webhook_v2 directly instead.
Deprecated:
This will route content to the v1 default tenant
"""
data = {
"title": title,
"content": content,
"email": email,
"sub": sub,
}
call_webhook_v1(payload=data, tenant_id=settings.v1_tenant_id)
def call_webhook_v2( def call_webhook_v2(
*, *,
tenant_id: str, tenant_id: str,
+1 -2
View File
@@ -4,7 +4,7 @@ import sentry_sdk
from fastapi import FastAPI from fastapi import FastAPI
from summary.api import health from summary.api import health
from summary.api.main import api_router_v1, api_router_v2 from summary.api.main import api_router_v2
from summary.core.config import get_settings from summary.core.config import get_settings
settings = get_settings() settings = get_settings()
@@ -17,6 +17,5 @@ app = FastAPI(
title=settings.app_name, title=settings.app_name,
) )
app.include_router(api_router_v1, prefix=settings.app_api_v1_str)
app.include_router(api_router_v2, prefix=settings.app_api_v2_str) app.include_router(api_router_v2, prefix=settings.app_api_v2_str)
app.include_router(health.router) app.include_router(health.router)
-1
View File
@@ -11,7 +11,6 @@ from summary.main import app
def get_settings_override(): def get_settings_override():
"""Return settings for tests.""" """Return settings for tests."""
return Settings( return Settings(
v1_tenant_id="test-tenant",
authorized_tenants=( authorized_tenants=(
AuthorizedTenant( AuthorizedTenant(
webhook_url="https://example.com/webhook", webhook_url="https://example.com/webhook",
+1475
View File
File diff suppressed because it is too large Load Diff