mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 04:39:06 +00:00
doc update (vibe)
This commit is contained in:
91
PLAN.md
91
PLAN.md
@@ -213,15 +213,28 @@ server/reflector/db/recordings.py # Recording model
|
|||||||
|
|
||||||
### Step 1.3: Define Standard Data Models
|
### 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`:**
|
**Create `server/reflector/video_platforms/models.py`:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal, Optional
|
from typing import Any, Dict, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from reflector.platform_types import Platform
|
||||||
Platform = Literal["whereby", "daily"]
|
|
||||||
|
|
||||||
|
|
||||||
class MeetingData(BaseModel):
|
class MeetingData(BaseModel):
|
||||||
@@ -976,7 +989,9 @@ DEFAULT_VIDEO_PLATFORM: Literal["whereby", "daily"] = "whereby" # Default to Wh
|
|||||||
|
|
||||||
### Step 2.8: Update Database Schema
|
### Step 2.8: Update Database Schema
|
||||||
|
|
||||||
**Create migration: `server/migrations/versions/YYYYMMDDHHMMSS_add_platform_support.py`**
|
**Create migration: `server/migrations/versions/<alembic-id>_add_platform_support.py`**
|
||||||
|
|
||||||
|
Note: Alembic generates revision IDs automatically (e.g., `1e49625677e4_add_platform_support.py`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd server
|
cd server
|
||||||
@@ -1239,21 +1254,22 @@ class DailyClient(VideoPlatformClient):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Configure recording if enabled
|
# Configure recording if enabled
|
||||||
if room.recording_type == "cloud":
|
# NOTE: Daily.co always uses "raw-tracks" for better transcription quality
|
||||||
data["properties"]["enable_recording"] = "cloud"
|
# (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 recording destination if bucket configured
|
# Configure S3 bucket for recordings
|
||||||
if self.config.s3_bucket and self.config.aws_role_arn:
|
# 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"] = {
|
data["properties"]["recordings_bucket"] = {
|
||||||
"bucket_name": self.config.s3_bucket,
|
"bucket_name": self.config.s3_bucket,
|
||||||
"bucket_region": self.config.s3_region,
|
"bucket_region": self.config.s3_region,
|
||||||
"assume_role_arn": self.config.aws_role_arn,
|
"assume_role_arn": self.config.aws_role_arn,
|
||||||
"allow_api_access": True,
|
"allow_api_access": True,
|
||||||
}
|
}
|
||||||
elif room.recording_type == "local":
|
|
||||||
data["properties"]["enable_recording"] = "local"
|
|
||||||
else:
|
|
||||||
data["properties"]["enable_recording"] = False
|
|
||||||
|
|
||||||
# Make API request
|
# Make API request
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -1370,8 +1386,51 @@ class DailyClient(VideoPlatformClient):
|
|||||||
|
|
||||||
# Constant-time comparison
|
# Constant-time comparison
|
||||||
return hmac.compare_digest(expected_sig, signature)
|
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
|
### Step 3.2: Register Daily.co Client
|
||||||
|
|
||||||
**Update `server/reflector/video_platforms/__init__.py`:**
|
**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
|
### 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`:**
|
**Update `server/reflector/worker/process.py`:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -2342,6 +2406,7 @@ export DEFAULT_VIDEO_PLATFORM=whereby
|
|||||||
## Appendix A: File Checklist
|
## Appendix A: File Checklist
|
||||||
|
|
||||||
### Backend Files (New)
|
### 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/__init__.py`
|
||||||
- [ ] `server/reflector/video_platforms/base.py`
|
- [ ] `server/reflector/video_platforms/base.py`
|
||||||
- [ ] `server/reflector/video_platforms/models.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/daily.py`
|
||||||
- [ ] `server/reflector/video_platforms/mock.py`
|
- [ ] `server/reflector/video_platforms/mock.py`
|
||||||
- [ ] `server/reflector/views/daily.py`
|
- [ ] `server/reflector/views/daily.py`
|
||||||
- [ ] `server/migrations/versions/YYYYMMDDHHMMSS_add_platform_support.py`
|
- [ ] `server/migrations/versions/<alembic-id>_add_platform_support.py` (e.g., `1e49625677e4_...`)
|
||||||
|
|
||||||
### Backend Files (Modified)
|
### Backend Files (Modified)
|
||||||
- [ ] `server/reflector/settings.py`
|
- [ ] `server/reflector/settings.py`
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ It allows seamless switching between providers (Whereby, Daily.co, etc.) without
|
|||||||
changing the core application logic.
|
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
|
from .registry import get_platform_client, register_platform
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -1,39 +1,14 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional
|
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from reflector.platform_types import Platform
|
from reflector.platform_types import Platform
|
||||||
|
|
||||||
|
from .models import MeetingData, VideoPlatformConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from reflector.db.rooms import Room
|
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):
|
class VideoPlatformClient(ABC):
|
||||||
"""Abstract base class for video platform integrations."""
|
"""Abstract base class for video platform integrations."""
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ class DailyClient(VideoPlatformClient):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Configure S3 bucket for recordings
|
# Configure S3 bucket for recordings
|
||||||
if room.recording_type != self.RECORDING_NONE and self.config.s3_bucket:
|
# 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"] = {
|
data["properties"]["recordings_bucket"] = {
|
||||||
"bucket_name": self.config.s3_bucket,
|
"bucket_name": self.config.s3_bucket,
|
||||||
"bucket_region": self.config.s3_region,
|
"bucket_region": self.config.s3_region,
|
||||||
|
|||||||
49
server/reflector/video_platforms/models.py
Normal file
49
server/reflector/video_platforms/models.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user