mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 18:56:58 +00:00
wip
This commit is contained in:
@@ -58,6 +58,9 @@ class NotificationService:
|
|||||||
email_success = self._notify_user_by_email(recording)
|
email_success = self._notify_user_by_email(recording)
|
||||||
return email_success and summary_success
|
return email_success and summary_success
|
||||||
|
|
||||||
|
if True: # TODO: add condition (in recording or global setting)
|
||||||
|
self._notify_push_file_service(recording)
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
"Unknown recording mode %s for recording %s",
|
"Unknown recording mode %s for recording %s",
|
||||||
recording.mode,
|
recording.mode,
|
||||||
@@ -222,6 +225,31 @@ class NotificationService:
|
|||||||
f"Unknown summary service version: {settings.SUMMARY_SERVICE_VERSION}"
|
f"Unknown summary service version: {settings.SUMMARY_SERVICE_VERSION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _notify_push_file_service(recording: models.Recording):
|
||||||
|
"""Notify push to file storage service."""
|
||||||
|
|
||||||
|
payload = {""}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {settings.SUMMARY_SERVICE_API_TOKEN}",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
"=http://app-summary-dev:8000/api/v2/async-jobs/push-recording/", # TODO, replace by: settings.PUSH_RECORDING_SERVICE_ENDPOINT,
|
||||||
|
json=payload,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
logger.exception("Push failed" # TODO: better messagea
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _notify_summary_service_v1(recording: models.Recording):
|
def _notify_summary_service_v1(recording: models.Recording):
|
||||||
"""Notify summary service about a new recording."""
|
"""Notify summary service about a new recording."""
|
||||||
@@ -386,7 +414,7 @@ class NotificationService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
settings.SUMMARY_SERVICE_ENDPOINT,
|
settings.SUMMARY_SERVICE_ENDPOINT, # process_audio_transcribe_v2_task
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
@@ -22,6 +23,7 @@ from summary.core.file_service import (
|
|||||||
CorruptedAudioFile,
|
CorruptedAudioFile,
|
||||||
FileService,
|
FileService,
|
||||||
FileServiceException,
|
FileServiceException,
|
||||||
|
SizedStream,
|
||||||
TranscribeError,
|
TranscribeError,
|
||||||
)
|
)
|
||||||
from summary.core.llm_service import LLMException, LLMObservability, LLMService
|
from summary.core.llm_service import LLMException, LLMObservability, LLMService
|
||||||
@@ -564,6 +566,104 @@ def process_audio_transcribe_v2_task(
|
|||||||
return success_payload.model_dump()
|
return success_payload.model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task(
|
||||||
|
bind=True,
|
||||||
|
autoretry_for=[exceptions.HTTPError],
|
||||||
|
max_retries=settings.celery_max_retries,
|
||||||
|
queue=settings.push_recording,
|
||||||
|
)
|
||||||
|
def push_recording(
|
||||||
|
self,
|
||||||
|
payload: dict,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new file in the main workspace.
|
||||||
|
|
||||||
|
The recording is streamed straight from object storage to Drive's presigned
|
||||||
|
URL: it is never fully downloaded to the worker's disk or memory.
|
||||||
|
|
||||||
|
Mostly taken from: https://github.com/suitenumerique/drive/blob/main/docs/resource_server.md#
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the access token from the session
|
||||||
|
access_token = payload['oidc_access_token']
|
||||||
|
cloud_storage_url = payload['cloud_storage_url']
|
||||||
|
filename = Path(urlparse(cloud_storage_url).path).name
|
||||||
|
|
||||||
|
# Get the main workspace
|
||||||
|
response = requests.get(
|
||||||
|
f"{settings.DRIVE_API}/items/",
|
||||||
|
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
items = data['results']
|
||||||
|
main_workspace = None
|
||||||
|
for item in items:
|
||||||
|
if item['main_workspace']:
|
||||||
|
main_workspace = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if not main_workspace:
|
||||||
|
raise Exception(status=404, data={"error": "No main workspace found"})
|
||||||
|
|
||||||
|
# Create a new file in the main workspace
|
||||||
|
response = requests.post(
|
||||||
|
f"{settings.DRIVE_API}/items/{main_workspace['id']}/children/",
|
||||||
|
json={
|
||||||
|
"type": "file",
|
||||||
|
"filename": filename,
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
item = response.json()
|
||||||
|
policy = item['policy']
|
||||||
|
|
||||||
|
# Upload file content using the presigned URL, streaming the recording
|
||||||
|
# straight from object storage: bytes are relayed chunk by chunk and the
|
||||||
|
# file is not fully held in memory.
|
||||||
|
with requests.get(
|
||||||
|
cloud_storage_url,
|
||||||
|
stream=True,
|
||||||
|
timeout=(10, 300),
|
||||||
|
) as download_response:
|
||||||
|
download_response.raise_for_status()
|
||||||
|
|
||||||
|
content_length = download_response.headers.get("Content-Length")
|
||||||
|
if content_length is None:
|
||||||
|
raise FileServiceException(
|
||||||
|
"Object storage did not return the recording size, "
|
||||||
|
"cannot stream it to Drive."
|
||||||
|
)
|
||||||
|
|
||||||
|
upload_response = requests.put(
|
||||||
|
policy,
|
||||||
|
data=SizedStream(download_response.raw, int(content_length)),
|
||||||
|
headers={
|
||||||
|
"Content-Type": download_response.headers.get(
|
||||||
|
"Content-Type", "application/octet-stream"
|
||||||
|
),
|
||||||
|
"Content-Length": content_length,
|
||||||
|
"x-amz-acl": "private",
|
||||||
|
},
|
||||||
|
timeout=(10, 300),
|
||||||
|
)
|
||||||
|
upload_response.raise_for_status()
|
||||||
|
|
||||||
|
# Tell the Drive API that the upload is ended
|
||||||
|
# drive_api = settings.DRIVE_API
|
||||||
|
drive_api = ""
|
||||||
|
response = requests.post(
|
||||||
|
f"{drive_api}/items/{item['id']}/upload-ended/",
|
||||||
|
json={},
|
||||||
|
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@signals.task_prerun.connect(sender=process_audio_transcribe_v2_task)
|
@signals.task_prerun.connect(sender=process_audio_transcribe_v2_task)
|
||||||
def task_started_transcript(task_id=None, task=None, args=None, **kwargs):
|
def task_started_transcript(task_id=None, task=None, args=None, **kwargs):
|
||||||
"""Signal handler called before task execution begins."""
|
"""Signal handler called before task execution begins."""
|
||||||
|
|||||||
@@ -72,10 +72,11 @@ 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
|
||||||
|
|
||||||
# v2 tasks
|
# 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"
|
||||||
call_webhook_queue_v2: str = "call-webhook-queue-v2"
|
call_webhook_queue_v2: str = "call-webhook-queue-v2"
|
||||||
|
push_recording: str = "push-recording"
|
||||||
|
|
||||||
# Minio settings
|
# Minio settings
|
||||||
aws_storage_bucket_name: str
|
aws_storage_bucket_name: str
|
||||||
|
|||||||
@@ -266,6 +266,33 @@ class FileServiceException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SizedStream:
|
||||||
|
"""Read-only byte stream of a known size, suitable as a `requests` body.
|
||||||
|
|
||||||
|
`requests` falls back to a chunked transfer encoding when it cannot guess
|
||||||
|
the body size upfront, which presigned S3 uploads reject. Advertising the
|
||||||
|
size through `__len__` makes it send a plain `Content-Length` instead,
|
||||||
|
while the underlying stream is still consumed chunk by chunk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream, length: int):
|
||||||
|
"""Wrap `stream`, whose full content is `length` bytes long."""
|
||||||
|
self._stream = stream
|
||||||
|
self._length = length
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Return the total size of the stream, in bytes."""
|
||||||
|
return self._length
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Iterate over the stream, required for `requests` to stream the body."""
|
||||||
|
return iter(self._stream)
|
||||||
|
|
||||||
|
def read(self, amt=None) -> bytes:
|
||||||
|
"""Read up to `amt` bytes from the stream."""
|
||||||
|
return self._stream.read(amt)
|
||||||
|
|
||||||
|
|
||||||
class FileService:
|
class FileService:
|
||||||
"""Service for downloading and preparing files from MinIO storage."""
|
"""Service for downloading and preparing files from MinIO storage."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user