mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-22 21:29:05 +00:00
feat: daily.co support as alternative to whereby (#691)
* llm instructions * vibe dailyco * vibe dailyco * doc update (vibe) * dont show recording ui on call * stub processor (vibe) * stub processor (vibe) self-review * stub processor (vibe) self-review * chore(main): release 0.14.0 (#670) * Add multitrack pipeline * Mixdown audio tracks * Mixdown with pyav filter graph * Trigger multitrack processing for daily recordings * apply platform from envs in priority: non-dry * Use explicit track keys for processing * Align tracks of a multitrack recording * Generate waveforms for the mixed audio * Emit multriack pipeline events * Fix multitrack pipeline track alignment * dailico docs * Enable multitrack reprocessing * modal temp files uniform names, cleanup. remove llm temporary docs * docs cleanup * dont proceed with raw recordings if any of the downloads fail * dry transcription pipelines * remove is_miltitrack * comments * explicit dailyco room name * docs * remove stub data/method * frontend daily/whereby code self-review (no-mistake) * frontend daily/whereby code self-review (no-mistakes) * frontend daily/whereby code self-review (no-mistakes) * consent cleanup for multitrack (no-mistakes) * llm fun * remove extra comments * fix tests * merge migrations * Store participant names * Get participants by meeting session id * pop back main branch migration * s3 paddington (no-mistakes) * comment * pr comments * pr comments * pr comments * platform / meeting cleanup * Use participant names in summary generation * platform assignment to meeting at controller level * pr comment * room playform properly default none * room playform properly default none * restore migration lost * streaming WIP * extract storage / use common storage / proper env vars for storage * fix mocks tests * remove fall back * streaming for multifile * cenrtal storage abstraction (no-mistakes) * remove dead code / vars * Set participant user id for authenticated users * whereby recording name parsing fix * whereby recording name parsing fix * more file stream * storage dry + tests * remove homemade boto3 streaming and use proper boto * update migration guide * webhook creation script - print uuid --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com> Co-authored-by: Mathieu Virbel <mat@meltingrocks.com> Co-authored-by: Sergey Mankovsky <sergey@monadical.com>
This commit is contained in:
@@ -7,7 +7,10 @@ from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from reflector.db import get_database, metadata
|
||||
from reflector.db.rooms import Room
|
||||
from reflector.schemas.platform import WHEREBY_PLATFORM, Platform
|
||||
from reflector.utils import generate_uuid4
|
||||
from reflector.utils.string import assert_equal
|
||||
from reflector.video_platforms.factory import get_platform
|
||||
|
||||
meetings = sa.Table(
|
||||
"meeting",
|
||||
@@ -55,6 +58,12 @@ meetings = sa.Table(
|
||||
),
|
||||
),
|
||||
sa.Column("calendar_metadata", JSONB),
|
||||
sa.Column(
|
||||
"platform",
|
||||
sa.String,
|
||||
nullable=False,
|
||||
server_default=assert_equal(WHEREBY_PLATFORM, "whereby"),
|
||||
),
|
||||
sa.Index("idx_meeting_room_id", "room_id"),
|
||||
sa.Index("idx_meeting_calendar_event", "calendar_event_id"),
|
||||
)
|
||||
@@ -94,13 +103,14 @@ class Meeting(BaseModel):
|
||||
is_locked: bool = False
|
||||
room_mode: Literal["normal", "group"] = "normal"
|
||||
recording_type: Literal["none", "local", "cloud"] = "cloud"
|
||||
recording_trigger: Literal[
|
||||
recording_trigger: Literal[ # whereby-specific
|
||||
"none", "prompt", "automatic", "automatic-2nd-participant"
|
||||
] = "automatic-2nd-participant"
|
||||
num_clients: int = 0
|
||||
is_active: bool = True
|
||||
calendar_event_id: str | None = None
|
||||
calendar_metadata: dict[str, Any] | None = None
|
||||
platform: Platform = WHEREBY_PLATFORM
|
||||
|
||||
|
||||
class MeetingController:
|
||||
@@ -130,6 +140,7 @@ class MeetingController:
|
||||
recording_trigger=room.recording_trigger,
|
||||
calendar_event_id=calendar_event_id,
|
||||
calendar_metadata=calendar_metadata,
|
||||
platform=get_platform(room.platform),
|
||||
)
|
||||
query = meetings.insert().values(**meeting.model_dump())
|
||||
await get_database().execute(query)
|
||||
@@ -137,7 +148,8 @@ class MeetingController:
|
||||
|
||||
async def get_all_active(self) -> list[Meeting]:
|
||||
query = meetings.select().where(meetings.c.is_active)
|
||||
return await get_database().fetch_all(query)
|
||||
results = await get_database().fetch_all(query)
|
||||
return [Meeting(**result) for result in results]
|
||||
|
||||
async def get_by_room_name(
|
||||
self,
|
||||
@@ -147,16 +159,14 @@ class MeetingController:
|
||||
Get a meeting by room name.
|
||||
For backward compatibility, returns the most recent meeting.
|
||||
"""
|
||||
end_date = getattr(meetings.c, "end_date")
|
||||
query = (
|
||||
meetings.select()
|
||||
.where(meetings.c.room_name == room_name)
|
||||
.order_by(end_date.desc())
|
||||
.order_by(meetings.c.end_date.desc())
|
||||
)
|
||||
result = await get_database().fetch_one(query)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return Meeting(**result)
|
||||
|
||||
async def get_active(self, room: Room, current_time: datetime) -> Meeting | None:
|
||||
@@ -179,7 +189,6 @@ class MeetingController:
|
||||
result = await get_database().fetch_one(query)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return Meeting(**result)
|
||||
|
||||
async def get_all_active_for_room(
|
||||
@@ -219,17 +228,27 @@ class MeetingController:
|
||||
return None
|
||||
return Meeting(**result)
|
||||
|
||||
async def get_by_id(self, meeting_id: str, **kwargs) -> Meeting | None:
|
||||
async def get_by_id(
|
||||
self, meeting_id: str, room: Room | None = None
|
||||
) -> Meeting | None:
|
||||
query = meetings.select().where(meetings.c.id == meeting_id)
|
||||
|
||||
if room:
|
||||
query = query.where(meetings.c.room_id == room.id)
|
||||
|
||||
result = await get_database().fetch_one(query)
|
||||
if not result:
|
||||
return None
|
||||
return Meeting(**result)
|
||||
|
||||
async def get_by_calendar_event(self, calendar_event_id: str) -> Meeting | None:
|
||||
async def get_by_calendar_event(
|
||||
self, calendar_event_id: str, room: Room
|
||||
) -> Meeting | None:
|
||||
query = meetings.select().where(
|
||||
meetings.c.calendar_event_id == calendar_event_id
|
||||
)
|
||||
if room:
|
||||
query = query.where(meetings.c.room_id == room.id)
|
||||
result = await get_database().fetch_one(query)
|
||||
if not result:
|
||||
return None
|
||||
@@ -239,6 +258,28 @@ class MeetingController:
|
||||
query = meetings.update().where(meetings.c.id == meeting_id).values(**kwargs)
|
||||
await get_database().execute(query)
|
||||
|
||||
async def increment_num_clients(self, meeting_id: str) -> None:
|
||||
"""Atomically increment participant count."""
|
||||
query = (
|
||||
meetings.update()
|
||||
.where(meetings.c.id == meeting_id)
|
||||
.values(num_clients=meetings.c.num_clients + 1)
|
||||
)
|
||||
await get_database().execute(query)
|
||||
|
||||
async def decrement_num_clients(self, meeting_id: str) -> None:
|
||||
"""Atomically decrement participant count (min 0)."""
|
||||
query = (
|
||||
meetings.update()
|
||||
.where(meetings.c.id == meeting_id)
|
||||
.values(
|
||||
num_clients=sa.case(
|
||||
(meetings.c.num_clients > 0, meetings.c.num_clients - 1), else_=0
|
||||
)
|
||||
)
|
||||
)
|
||||
await get_database().execute(query)
|
||||
|
||||
|
||||
class MeetingConsentController:
|
||||
async def get_by_meeting_id(self, meeting_id: str) -> list[MeetingConsent]:
|
||||
|
||||
@@ -21,6 +21,7 @@ recordings = sa.Table(
|
||||
server_default="pending",
|
||||
),
|
||||
sa.Column("meeting_id", sa.String),
|
||||
sa.Column("track_keys", sa.JSON, nullable=True),
|
||||
sa.Index("idx_recording_meeting_id", "meeting_id"),
|
||||
)
|
||||
|
||||
@@ -28,10 +29,13 @@ recordings = sa.Table(
|
||||
class Recording(BaseModel):
|
||||
id: str = Field(default_factory=generate_uuid4)
|
||||
bucket_name: str
|
||||
# for single-track
|
||||
object_key: str
|
||||
recorded_at: datetime
|
||||
status: Literal["pending", "processing", "completed", "failed"] = "pending"
|
||||
meeting_id: str | None = None
|
||||
# for multitrack reprocessing
|
||||
track_keys: list[str] | None = None
|
||||
|
||||
|
||||
class RecordingController:
|
||||
|
||||
@@ -9,6 +9,7 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.sql import false, or_
|
||||
|
||||
from reflector.db import get_database, metadata
|
||||
from reflector.schemas.platform import Platform
|
||||
from reflector.utils import generate_uuid4
|
||||
|
||||
rooms = sqlalchemy.Table(
|
||||
@@ -50,6 +51,12 @@ rooms = sqlalchemy.Table(
|
||||
),
|
||||
sqlalchemy.Column("ics_last_sync", sqlalchemy.DateTime(timezone=True)),
|
||||
sqlalchemy.Column("ics_last_etag", sqlalchemy.Text),
|
||||
sqlalchemy.Column(
|
||||
"platform",
|
||||
sqlalchemy.String,
|
||||
nullable=True,
|
||||
server_default=None,
|
||||
),
|
||||
sqlalchemy.Index("idx_room_is_shared", "is_shared"),
|
||||
sqlalchemy.Index("idx_room_ics_enabled", "ics_enabled"),
|
||||
)
|
||||
@@ -66,7 +73,7 @@ class Room(BaseModel):
|
||||
is_locked: bool = False
|
||||
room_mode: Literal["normal", "group"] = "normal"
|
||||
recording_type: Literal["none", "local", "cloud"] = "cloud"
|
||||
recording_trigger: Literal[
|
||||
recording_trigger: Literal[ # whereby-specific
|
||||
"none", "prompt", "automatic", "automatic-2nd-participant"
|
||||
] = "automatic-2nd-participant"
|
||||
is_shared: bool = False
|
||||
@@ -77,6 +84,7 @@ class Room(BaseModel):
|
||||
ics_enabled: bool = False
|
||||
ics_last_sync: datetime | None = None
|
||||
ics_last_etag: str | None = None
|
||||
platform: Platform | None = None
|
||||
|
||||
|
||||
class RoomController:
|
||||
@@ -130,6 +138,7 @@ class RoomController:
|
||||
ics_url: str | None = None,
|
||||
ics_fetch_interval: int = 300,
|
||||
ics_enabled: bool = False,
|
||||
platform: Platform | None = None,
|
||||
):
|
||||
"""
|
||||
Add a new room
|
||||
@@ -153,6 +162,7 @@ class RoomController:
|
||||
ics_url=ics_url,
|
||||
ics_fetch_interval=ics_fetch_interval,
|
||||
ics_enabled=ics_enabled,
|
||||
platform=platform,
|
||||
)
|
||||
query = rooms.insert().values(**room.model_dump())
|
||||
try:
|
||||
|
||||
@@ -21,7 +21,7 @@ from reflector.db.utils import is_postgresql
|
||||
from reflector.logger import logger
|
||||
from reflector.processors.types import Word as ProcessorWord
|
||||
from reflector.settings import settings
|
||||
from reflector.storage import get_recordings_storage, get_transcripts_storage
|
||||
from reflector.storage import get_transcripts_storage
|
||||
from reflector.utils import generate_uuid4
|
||||
from reflector.utils.webvtt import topics_to_webvtt
|
||||
|
||||
@@ -186,6 +186,7 @@ class TranscriptParticipant(BaseModel):
|
||||
id: str = Field(default_factory=generate_uuid4)
|
||||
speaker: int | None
|
||||
name: str
|
||||
user_id: str | None = None
|
||||
|
||||
|
||||
class Transcript(BaseModel):
|
||||
@@ -623,7 +624,9 @@ class TranscriptController:
|
||||
)
|
||||
if recording:
|
||||
try:
|
||||
await get_recordings_storage().delete_file(recording.object_key)
|
||||
await get_transcripts_storage().delete_file(
|
||||
recording.object_key, bucket=recording.bucket_name
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to delete recording object from S3",
|
||||
@@ -725,11 +728,13 @@ class TranscriptController:
|
||||
"""
|
||||
Download audio from storage
|
||||
"""
|
||||
transcript.audio_mp3_filename.write_bytes(
|
||||
await get_transcripts_storage().get_file(
|
||||
transcript.storage_audio_path,
|
||||
)
|
||||
)
|
||||
storage = get_transcripts_storage()
|
||||
try:
|
||||
with open(transcript.audio_mp3_filename, "wb") as f:
|
||||
await storage.stream_to_fileobj(transcript.storage_audio_path, f)
|
||||
except Exception:
|
||||
transcript.audio_mp3_filename.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
async def upsert_participant(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user