This commit is contained in:
leo
2026-07-29 14:43:04 +02:00
parent 7c92f6054b
commit ac2b5bd4f3
4 changed files with 159 additions and 3 deletions
@@ -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,
+101 -1
View File
@@ -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."""
+2 -1
View File
@@ -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
+27
View File
@@ -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."""