From 0fcf8b6875927ac28cd98cb1f6bc65cb87931d2f Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Fri, 10 Oct 2025 10:57:35 -0400 Subject: [PATCH] doc update (vibe) --- PLAN.md | 101 +++++++++++++++---- server/reflector/video_platforms/__init__.py | 3 +- server/reflector/video_platforms/base.py | 31 +----- server/reflector/video_platforms/daily.py | 15 +-- server/reflector/video_platforms/models.py | 49 +++++++++ 5 files changed, 145 insertions(+), 54 deletions(-) create mode 100644 server/reflector/video_platforms/models.py diff --git a/PLAN.md b/PLAN.md index 6ab90da6..4c3ba681 100644 --- a/PLAN.md +++ b/PLAN.md @@ -213,15 +213,28 @@ server/reflector/db/recordings.py # Recording model ### Step 1.3: Define Standard Data Models +**Create `server/reflector/platform_types.py` (separate file to avoid circular imports):** + +```python +"""Platform type definitions. + +Separate file to prevent circular import issues when db models and +video platform code need to reference the Platform type. +""" + +from typing import Literal + +Platform = Literal["whereby", "daily"] +``` + **Create `server/reflector/video_platforms/models.py`:** ```python from datetime import datetime -from typing import Literal, Optional +from typing import Any, Dict, Optional from pydantic import BaseModel, Field - -Platform = Literal["whereby", "daily"] +from reflector.platform_types import Platform class MeetingData(BaseModel): @@ -976,7 +989,9 @@ DEFAULT_VIDEO_PLATFORM: Literal["whereby", "daily"] = "whereby" # Default to Wh ### Step 2.8: Update Database Schema -**Create migration: `server/migrations/versions/YYYYMMDDHHMMSS_add_platform_support.py`** +**Create migration: `server/migrations/versions/_add_platform_support.py`** + +Note: Alembic generates revision IDs automatically (e.g., `1e49625677e4_add_platform_support.py`) ```bash cd server @@ -1239,22 +1254,23 @@ class DailyClient(VideoPlatformClient): } # Configure recording if enabled - if room.recording_type == "cloud": - data["properties"]["enable_recording"] = "cloud" - - # Configure S3 recording destination if bucket configured - if self.config.s3_bucket and self.config.aws_role_arn: - data["properties"]["recordings_bucket"] = { - "bucket_name": self.config.s3_bucket, - "bucket_region": self.config.s3_region, - "assume_role_arn": self.config.aws_role_arn, - "allow_api_access": True, - } - elif room.recording_type == "local": - data["properties"]["enable_recording"] = "local" + # NOTE: Daily.co always uses "raw-tracks" for better transcription quality + # (multiple WebM files instead of single MP4) + if room.recording_type != "none": + data["properties"]["enable_recording"] = "raw-tracks" else: data["properties"]["enable_recording"] = False + # Configure S3 bucket for recordings + # NOTE: Not checking room.recording_type - figure out later if conditional needed + assert self.config.s3_bucket, "S3 bucket must be configured" + data["properties"]["recordings_bucket"] = { + "bucket_name": self.config.s3_bucket, + "bucket_region": self.config.s3_region, + "assume_role_arn": self.config.aws_role_arn, + "allow_api_access": True, + } + # Make API request async with httpx.AsyncClient() as client: response = await client.post( @@ -1370,8 +1386,51 @@ class DailyClient(VideoPlatformClient): # Constant-time comparison return hmac.compare_digest(expected_sig, signature) + + async def create_meeting_token(self, room_name: str, enable_recording: bool) -> str: + """Create JWT meeting token with optional auto-recording. + + Daily.co supports token-based meeting configuration, which allows + per-participant settings like auto-starting cloud recording. + + This is used instead of room-level recording config for more control. + """ + data = {"properties": {"room_name": room_name}} + + if enable_recording: + data["properties"]["start_cloud_recording"] = True + + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.BASE_URL}/meeting-tokens", + headers=self.headers, + json=data, + timeout=self.TIMEOUT, + ) + response.raise_for_status() + return response.json()["token"] ``` +**Token-Based Auto-Recording (Critical Addition)** + +After creating a Daily.co meeting, append JWT token to URLs for auto-recording: + +```python +# In rooms.py after create_meeting +if meeting.platform == "daily" and room.recording_trigger != "none": + client = create_platform_client(meeting.platform) + token = await client.create_meeting_token( + meeting.room_name, enable_recording=True + ) + meeting.room_url += f"?t={token}" + meeting.host_room_url += f"?t={token}" +``` + +**Why tokens instead of room config:** +- Room-level `enable_recording` only enables the capability +- Token with `start_cloud_recording: true` actually starts it +- Provides per-participant control (future: host-only recording) + ### Step 3.2: Register Daily.co Client **Update `server/reflector/video_platforms/__init__.py`:** @@ -1585,6 +1644,11 @@ app.include_router(daily.router, prefix="/v1/daily", tags=["daily"]) ### Step 3.4: Create Recording Processing Task +**⚠️ NEXT STEP - NOT YET IMPLEMENTED** + +The `process_recording_from_url` task described below is the next implementation step. +It handles downloading Daily.co recordings from webhook URLs into the existing transcription pipeline. + **Update `server/reflector/worker/process.py`:** ```python @@ -2342,6 +2406,7 @@ export DEFAULT_VIDEO_PLATFORM=whereby ## Appendix A: File Checklist ### Backend Files (New) +- [ ] `server/reflector/platform_types.py` (Platform literal type - separate to avoid circular imports) - [ ] `server/reflector/video_platforms/__init__.py` - [ ] `server/reflector/video_platforms/base.py` - [ ] `server/reflector/video_platforms/models.py` @@ -2351,7 +2416,7 @@ export DEFAULT_VIDEO_PLATFORM=whereby - [ ] `server/reflector/video_platforms/daily.py` - [ ] `server/reflector/video_platforms/mock.py` - [ ] `server/reflector/views/daily.py` -- [ ] `server/migrations/versions/YYYYMMDDHHMMSS_add_platform_support.py` +- [ ] `server/migrations/versions/_add_platform_support.py` (e.g., `1e49625677e4_...`) ### Backend Files (Modified) - [ ] `server/reflector/settings.py` diff --git a/server/reflector/video_platforms/__init__.py b/server/reflector/video_platforms/__init__.py index ded6244c..8dcd6348 100644 --- a/server/reflector/video_platforms/__init__.py +++ b/server/reflector/video_platforms/__init__.py @@ -5,7 +5,8 @@ It allows seamless switching between providers (Whereby, Daily.co, etc.) without changing the core application logic. """ -from .base import MeetingData, VideoPlatformClient, VideoPlatformConfig +from .base import VideoPlatformClient +from .models import MeetingData, VideoPlatformConfig from .registry import get_platform_client, register_platform __all__ = [ diff --git a/server/reflector/video_platforms/base.py b/server/reflector/video_platforms/base.py index c902bd6c..9d97afea 100644 --- a/server/reflector/video_platforms/base.py +++ b/server/reflector/video_platforms/base.py @@ -1,39 +1,14 @@ from abc import ABC, abstractmethod from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional - -from pydantic import BaseModel +from typing import TYPE_CHECKING, Any, Dict, Optional from reflector.platform_types import Platform +from .models import MeetingData, VideoPlatformConfig + if TYPE_CHECKING: from reflector.db.rooms import Room -RecordingType = Literal["none", "local", "cloud"] - - -class MeetingData(BaseModel): - meeting_id: str - room_name: str - room_url: str - host_room_url: str - platform: Platform - extra_data: Dict[str, Any] = {} - - -class VideoPlatformConfig(BaseModel): - """Configuration for a video platform.""" - - api_key: str - webhook_secret: str - api_url: Optional[str] = None - subdomain: Optional[str] = None - s3_bucket: Optional[str] = None - s3_region: Optional[str] = None - aws_role_arn: Optional[str] = None - aws_access_key_id: Optional[str] = None - aws_access_key_secret: Optional[str] = None - class VideoPlatformClient(ABC): """Abstract base class for video platform integrations.""" diff --git a/server/reflector/video_platforms/daily.py b/server/reflector/video_platforms/daily.py index a3bfc78d..74a8e8fb 100644 --- a/server/reflector/video_platforms/daily.py +++ b/server/reflector/video_platforms/daily.py @@ -58,13 +58,14 @@ class DailyClient(VideoPlatformClient): } # Configure S3 bucket for recordings - if room.recording_type != self.RECORDING_NONE and self.config.s3_bucket: - data["properties"]["recordings_bucket"] = { - "bucket_name": self.config.s3_bucket, - "bucket_region": self.config.s3_region, - "assume_role_arn": self.config.aws_role_arn, - "allow_api_access": True, - } + # NOTE: Not checking room.recording_type - figure out later if conditional needed + assert self.config.s3_bucket, "S3 bucket must be configured" + data["properties"]["recordings_bucket"] = { + "bucket_name": self.config.s3_bucket, + "bucket_region": self.config.s3_region, + "assume_role_arn": self.config.aws_role_arn, + "allow_api_access": True, + } from reflector.logger import logger diff --git a/server/reflector/video_platforms/models.py b/server/reflector/video_platforms/models.py new file mode 100644 index 00000000..af07226e --- /dev/null +++ b/server/reflector/video_platforms/models.py @@ -0,0 +1,49 @@ +"""Video platform data models. + +Standard data models used across all video platform implementations. +""" + +from typing import Any, Dict, Literal, Optional + +from pydantic import BaseModel, Field + +from reflector.platform_types import Platform + +RecordingType = Literal["none", "local", "cloud"] + + +class MeetingData(BaseModel): + """Standardized meeting data returned by all providers.""" + + platform: Platform + meeting_id: str = Field(description="Platform-specific meeting identifier") + room_url: str = Field(description="URL for participants to join") + host_room_url: str = Field(description="URL for hosts (may be same as room_url)") + room_name: str = Field(description="Human-readable room name") + extra_data: Dict[str, Any] = Field(default_factory=dict) + + class Config: + json_schema_extra = { + "example": { + "platform": "whereby", + "meeting_id": "12345678", + "room_url": "https://subdomain.whereby.com/room-20251008120000", + "host_room_url": "https://subdomain.whereby.com/room-20251008120000?roomKey=abc123", + "room_name": "room-20251008120000", + } + } + + +class VideoPlatformConfig(BaseModel): + """Platform-agnostic configuration model.""" + + api_key: str + webhook_secret: str + api_url: Optional[str] = None + subdomain: Optional[str] = None # Whereby/Daily subdomain + s3_bucket: Optional[str] = None + s3_region: Optional[str] = None + # Whereby uses access keys, Daily uses IAM role + aws_access_key_id: Optional[str] = None + aws_access_key_secret: Optional[str] = None + aws_role_arn: Optional[str] = None