mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-17 14:07:49 +00:00
♻️(summary) refactor tasks signature and make transcription tz-aware
The tasks endpoint used non-timezone-aware date and time values and split them into separate variables, which is unconventional. Refactor the implementation to use timezone-aware datetime objects and align transcription formatting with the user-declared timezone. Update the source of truth for recording start time to FileInfo.started_at for improved precision. Adjust the task signature in preparation for upcoming user assignment work, which will require `started_at`, `ended_at`, and `metadata_filename`.
This commit is contained in:
@@ -19,16 +19,18 @@ class TranscribeSummarizeTaskCreation(BaseModel):
|
||||
"""Transcription and summarization parameters."""
|
||||
|
||||
owner_id: str
|
||||
filename: str
|
||||
recording_filename: str
|
||||
metadata_filename: Optional[str] = None
|
||||
email: str
|
||||
sub: str
|
||||
version: Optional[int] = 2
|
||||
room: Optional[str]
|
||||
recording_date: Optional[str]
|
||||
recording_time: 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
|
||||
@@ -51,16 +53,18 @@ async def create_transcribe_summarize_task(request: TranscribeSummarizeTaskCreat
|
||||
task = process_audio_transcribe_summarize_v2.apply_async(
|
||||
args=[
|
||||
request.owner_id,
|
||||
request.filename,
|
||||
request.recording_filename,
|
||||
request.metadata_filename,
|
||||
request.email,
|
||||
request.sub,
|
||||
time.time(),
|
||||
request.room,
|
||||
request.recording_date,
|
||||
request.recording_time,
|
||||
request.owner_timezone,
|
||||
request.language,
|
||||
request.download_link,
|
||||
request.context_language,
|
||||
request.recording_start_at,
|
||||
request.recording_end_at,
|
||||
],
|
||||
queue=settings.transcribe_queue,
|
||||
)
|
||||
|
||||
@@ -112,7 +112,9 @@ class MetadataManager:
|
||||
if self._is_disabled or self.has_task_id(task_id):
|
||||
return
|
||||
|
||||
_, filename, email, _, received_at, *_ = task_args
|
||||
# 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()
|
||||
initial_metadata = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import openai
|
||||
import sentry_sdk
|
||||
@@ -78,7 +77,7 @@ file_service = FileService()
|
||||
def transcribe_audio(
|
||||
*,
|
||||
task_id: str,
|
||||
filename: str | None = None,
|
||||
recording_filename: str | None = None,
|
||||
language: str,
|
||||
cloud_storage_url=None,
|
||||
raises: bool = False,
|
||||
@@ -90,7 +89,7 @@ def transcribe_audio(
|
||||
|
||||
Returns the transcription object, or None if the file could not be retrieved.
|
||||
"""
|
||||
if bool(filename) == bool(cloud_storage_url):
|
||||
if bool(recording_filename) == bool(cloud_storage_url):
|
||||
raise ValueError(
|
||||
"Either filename or cloud_storage_url must be provided, but not both."
|
||||
)
|
||||
@@ -105,11 +104,12 @@ def transcribe_audio(
|
||||
# Transcription
|
||||
try:
|
||||
with file_service.prepare_audio_file(
|
||||
remote_object_key=filename,
|
||||
remote_object_key=recording_filename,
|
||||
cloud_storage_url=cloud_storage_url,
|
||||
) as (audio_file, metadata):
|
||||
metadata_manager.track(task_id, {"audio_length": metadata["duration"]})
|
||||
|
||||
# Compute language parameter
|
||||
if language is None:
|
||||
language = settings.whisperx_default_language
|
||||
logger.info(
|
||||
@@ -122,18 +122,21 @@ def transcribe_audio(
|
||||
language,
|
||||
)
|
||||
|
||||
# Call remote service for transcription
|
||||
transcription_start_time = time.time()
|
||||
|
||||
transcription = whisperx_client.audio.transcriptions.create(
|
||||
model=settings.whisperx_asr_model, file=audio_file, language=language
|
||||
)
|
||||
|
||||
transcription_time = round(time.time() - transcription_start_time, 2)
|
||||
# Logging
|
||||
transcription_duration = round(time.time() - transcription_start_time, 2)
|
||||
metadata_manager.track(
|
||||
task_id,
|
||||
{"transcription_time": transcription_time},
|
||||
{"transcription_time": transcription_duration},
|
||||
)
|
||||
logger.info(
|
||||
"Transcription received in %.2f seconds.", transcription_duration
|
||||
)
|
||||
logger.info("Transcription received in %.2f seconds.", transcription_time)
|
||||
logger.debug("Transcription: \n %s", transcription)
|
||||
|
||||
except FileServiceException as e:
|
||||
@@ -148,7 +151,7 @@ def transcribe_audio(
|
||||
"Unexpected error while preparing file | filename: %s "
|
||||
"| cloud_storage_url: %s"
|
||||
),
|
||||
filename,
|
||||
recording_filename,
|
||||
redacted_cloud_storage_url,
|
||||
)
|
||||
return None
|
||||
@@ -162,8 +165,8 @@ def format_transcript(
|
||||
context_language: str | None,
|
||||
language: str,
|
||||
room: str | None,
|
||||
recording_date: str | None,
|
||||
recording_time: str | None,
|
||||
recording_datetime: str | None,
|
||||
owner_timezone: str | None,
|
||||
download_link: str | None,
|
||||
) -> tuple[str, str]:
|
||||
"""Format a transcription into readable content with a title.
|
||||
@@ -179,8 +182,8 @@ def format_transcript(
|
||||
return formatter.format(
|
||||
transcription,
|
||||
room=room,
|
||||
recording_date=recording_date,
|
||||
recording_time=recording_time,
|
||||
recording_datetime=recording_datetime,
|
||||
owner_timezone=owner_timezone,
|
||||
download_link=download_link,
|
||||
)
|
||||
|
||||
@@ -188,7 +191,7 @@ def format_transcript(
|
||||
def format_actions(llm_output: dict) -> str:
|
||||
"""Format the actions from the LLM output into a markdown list.
|
||||
|
||||
fomat:
|
||||
format:
|
||||
- [ ] Action title Assignée à : assignee1, assignee2, Échéance : due_date
|
||||
"""
|
||||
lines = []
|
||||
@@ -212,16 +215,18 @@ def format_actions(llm_output: dict) -> str:
|
||||
def process_audio_transcribe_summarize_v2(
|
||||
self,
|
||||
owner_id: str,
|
||||
filename: str,
|
||||
recording_filename: str,
|
||||
metadata_filename: str | None,
|
||||
email: str,
|
||||
sub: str,
|
||||
received_at: float,
|
||||
room: Optional[str],
|
||||
recording_date: Optional[str],
|
||||
recording_time: Optional[str],
|
||||
language: Optional[str],
|
||||
download_link: Optional[str],
|
||||
context_language: Optional[str] = None,
|
||||
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.
|
||||
|
||||
@@ -234,16 +239,20 @@ def process_audio_transcribe_summarize_v2(
|
||||
Args:
|
||||
self: Celery task instance (passed on with bind=True)
|
||||
owner_id: Unique identifier of the recording owner.
|
||||
filename: Name of the audio file in MinIO storage.
|
||||
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.
|
||||
recording_date: Date of the recording (localized display string).
|
||||
recording_time: Time of the recording (localized display string).
|
||||
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",
|
||||
@@ -253,19 +262,21 @@ def process_audio_transcribe_summarize_v2(
|
||||
|
||||
task_id = self.request.id
|
||||
|
||||
# Transcribe the audio
|
||||
transcription = transcribe_audio(
|
||||
task_id=task_id, filename=filename, language=language
|
||||
task_id=task_id, recording_filename=recording_filename, language=language
|
||||
)
|
||||
if transcription is None:
|
||||
return
|
||||
|
||||
# Format output
|
||||
content, title = format_transcript(
|
||||
transcription,
|
||||
context_language,
|
||||
language,
|
||||
room,
|
||||
recording_date,
|
||||
recording_time,
|
||||
recording_start_at,
|
||||
owner_timezone,
|
||||
download_link,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Transcript formatting into readable conversation format with speaker labels."""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from summary.core.config import get_settings
|
||||
from summary.core.locales import LocaleStrings
|
||||
@@ -39,10 +41,10 @@ class TranscriptFormatter:
|
||||
def format(
|
||||
self,
|
||||
transcription,
|
||||
room: Optional[str] = None,
|
||||
recording_date: Optional[str] = None,
|
||||
recording_time: Optional[str] = None,
|
||||
download_link: Optional[str] = None,
|
||||
room: str | None = None,
|
||||
recording_datetime: str | None = None,
|
||||
owner_timezone: str | None = None,
|
||||
download_link: str | None = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""Format transcription into the final document and its title."""
|
||||
segments = self._get_segments(transcription)
|
||||
@@ -54,7 +56,7 @@ class TranscriptFormatter:
|
||||
content = self._remove_hallucinations(content)
|
||||
content = self._add_header(content, download_link)
|
||||
|
||||
title = self._generate_title(room, recording_date, recording_time)
|
||||
title = self._generate_title(room, recording_datetime, owner_timezone)
|
||||
|
||||
return content, title
|
||||
|
||||
@@ -83,7 +85,7 @@ class TranscriptFormatter:
|
||||
|
||||
return formatted_output
|
||||
|
||||
def _add_header(self, content, download_link: Optional[str]) -> str:
|
||||
def _add_header(self, content, download_link: str | None) -> str:
|
||||
"""Add download link header to the document content."""
|
||||
if not download_link:
|
||||
return content
|
||||
@@ -97,16 +99,20 @@ class TranscriptFormatter:
|
||||
|
||||
def _generate_title(
|
||||
self,
|
||||
room: Optional[str] = None,
|
||||
recording_date: Optional[str] = None,
|
||||
recording_time: Optional[str] = None,
|
||||
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_date or not recording_time:
|
||||
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=recording_date,
|
||||
room_recording_time=recording_time,
|
||||
room_recording_date=dt.strftime("%Y-%m-%d"),
|
||||
room_recording_time=dt.strftime("%H:%M"),
|
||||
)
|
||||
|
||||
@@ -19,14 +19,14 @@ class TestTasks:
|
||||
headers={"Authorization": "Bearer test-api-token"},
|
||||
json={
|
||||
"owner_id": "owner-123",
|
||||
"filename": "recording.mp4",
|
||||
"recording_filename": "recording.mp4",
|
||||
"metadata_filename": "metadata.json",
|
||||
"email": "user@example.com",
|
||||
"sub": "sub-123",
|
||||
"room": "room-abc",
|
||||
"recording_date": "2026-01-01",
|
||||
"recording_time": "10:00:00",
|
||||
"owner_timezone": "UTC",
|
||||
"language": None,
|
||||
"download_link": "http://example.com/file.mp4",
|
||||
"download_link": "https://example.com/file.mp4",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -36,16 +36,18 @@ class TestTasks:
|
||||
args = mock_apply_async.call_args.kwargs["args"]
|
||||
assert args == [
|
||||
"owner-123", # owner_id
|
||||
"recording.mp4", # filename
|
||||
"recording.mp4", # recording_filename
|
||||
"metadata.json", # metadata_filename
|
||||
"user@example.com", # email
|
||||
"sub-123", # sub
|
||||
1735725600.0, # frozen time
|
||||
1735725600.0, # received_at
|
||||
"room-abc", # room
|
||||
"2026-01-01", # recording_date
|
||||
"10:00:00", # recording_time
|
||||
"UTC", # owner_timezone
|
||||
None, # language
|
||||
"http://example.com/file.mp4", # download_link
|
||||
"https://example.com/file.mp4", # download_link
|
||||
None, # context_language
|
||||
None, # recording_start_at
|
||||
None, # recording_end_at
|
||||
]
|
||||
|
||||
def test_create_task_invalid_language(self, client):
|
||||
|
||||
Reference in New Issue
Block a user