mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-22 05:09:05 +00:00
server: add participant API
Also break out views into different files for easier reading
This commit is contained in:
@@ -1,33 +1,19 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Annotated, Literal, Optional
|
||||
|
||||
import httpx
|
||||
import reflector.auth as auth
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
status,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.databases import paginate
|
||||
from jose import jwt
|
||||
from pydantic import BaseModel, Field
|
||||
from reflector.db.transcripts import (
|
||||
AudioWaveform,
|
||||
TranscriptParticipant,
|
||||
TranscriptTopic,
|
||||
transcripts_controller,
|
||||
)
|
||||
from reflector.processors.types import Transcript as ProcessorTranscript
|
||||
from reflector.settings import settings
|
||||
from reflector.ws_manager import get_ws_manager
|
||||
|
||||
from ._range_requests_response import range_requests_response
|
||||
from .rtc_offer import RtcOffer, rtc_offer_base
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -62,6 +48,7 @@ class GetTranscript(BaseModel):
|
||||
share_mode: str = Field("private")
|
||||
source_language: str | None
|
||||
target_language: str | None
|
||||
participants: list[TranscriptParticipant] | None
|
||||
|
||||
|
||||
class CreateTranscript(BaseModel):
|
||||
@@ -77,6 +64,7 @@ class UpdateTranscript(BaseModel):
|
||||
short_summary: Optional[str] = Field(None)
|
||||
long_summary: Optional[str] = Field(None)
|
||||
share_mode: Optional[Literal["public", "semi-private", "private"]] = Field(None)
|
||||
participants: Optional[list[TranscriptParticipant]] = Field(None)
|
||||
|
||||
|
||||
class DeletionStatus(BaseModel):
|
||||
@@ -192,19 +180,7 @@ async def transcript_update(
|
||||
transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id)
|
||||
if not transcript:
|
||||
raise HTTPException(status_code=404, detail="Transcript not found")
|
||||
values = {}
|
||||
if info.name is not None:
|
||||
values["name"] = info.name
|
||||
if info.locked is not None:
|
||||
values["locked"] = info.locked
|
||||
if info.long_summary is not None:
|
||||
values["long_summary"] = info.long_summary
|
||||
if info.short_summary is not None:
|
||||
values["short_summary"] = info.short_summary
|
||||
if info.title is not None:
|
||||
values["title"] = info.title
|
||||
if info.share_mode is not None:
|
||||
values["share_mode"] = info.share_mode
|
||||
values = info.dict(exclude_unset=True)
|
||||
await transcripts_controller.update(transcript, values)
|
||||
return transcript
|
||||
|
||||
@@ -222,97 +198,6 @@ async def transcript_delete(
|
||||
return DeletionStatus(status="ok")
|
||||
|
||||
|
||||
@router.get("/transcripts/{transcript_id}/audio/mp3")
|
||||
@router.head("/transcripts/{transcript_id}/audio/mp3")
|
||||
async def transcript_get_audio_mp3(
|
||||
request: Request,
|
||||
transcript_id: str,
|
||||
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||
token: str | None = None,
|
||||
):
|
||||
user_id = user["sub"] if user else None
|
||||
if not user_id and token:
|
||||
unauthorized_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
except jwt.JWTError:
|
||||
raise unauthorized_exception
|
||||
|
||||
transcript = await transcripts_controller.get_by_id_for_http(
|
||||
transcript_id, user_id=user_id
|
||||
)
|
||||
|
||||
if transcript.audio_location == "storage":
|
||||
# proxy S3 file, to prevent issue with CORS
|
||||
url = await transcript.get_audio_url()
|
||||
headers = {}
|
||||
|
||||
copy_headers = ["range", "accept-encoding"]
|
||||
for header in copy_headers:
|
||||
if header in request.headers:
|
||||
headers[header] = request.headers[header]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.request(request.method, url, headers=headers)
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
headers=resp.headers,
|
||||
)
|
||||
|
||||
if transcript.audio_location == "storage":
|
||||
# proxy S3 file, to prevent issue with CORS
|
||||
url = await transcript.get_audio_url()
|
||||
headers = {}
|
||||
|
||||
copy_headers = ["range", "accept-encoding"]
|
||||
for header in copy_headers:
|
||||
if header in request.headers:
|
||||
headers[header] = request.headers[header]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.request(request.method, url, headers=headers)
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
headers=resp.headers,
|
||||
)
|
||||
|
||||
if not transcript.audio_mp3_filename.exists():
|
||||
raise HTTPException(status_code=500, detail="Audio not found")
|
||||
|
||||
truncated_id = str(transcript.id).split("-")[0]
|
||||
filename = f"recording_{truncated_id}.mp3"
|
||||
|
||||
return range_requests_response(
|
||||
request,
|
||||
transcript.audio_mp3_filename,
|
||||
content_type="audio/mpeg",
|
||||
content_disposition=f"attachment; filename={filename}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/transcripts/{transcript_id}/audio/waveform")
|
||||
async def transcript_get_audio_waveform(
|
||||
transcript_id: str,
|
||||
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||
) -> AudioWaveform:
|
||||
user_id = user["sub"] if user else None
|
||||
transcript = await transcripts_controller.get_by_id_for_http(
|
||||
transcript_id, user_id=user_id
|
||||
)
|
||||
|
||||
if not transcript.audio_waveform_filename.exists():
|
||||
raise HTTPException(status_code=404, detail="Audio not found")
|
||||
|
||||
return transcript.audio_waveform
|
||||
|
||||
|
||||
@router.get(
|
||||
"/transcripts/{transcript_id}/topics",
|
||||
response_model=list[GetTranscriptTopic],
|
||||
@@ -330,84 +215,3 @@ async def transcript_get_topics(
|
||||
return [
|
||||
GetTranscriptTopic.from_transcript_topic(topic) for topic in transcript.topics
|
||||
]
|
||||
|
||||
|
||||
# ==============================================================
|
||||
# Websocket
|
||||
# ==============================================================
|
||||
|
||||
|
||||
@router.get("/transcripts/{transcript_id}/events")
|
||||
async def transcript_get_websocket_events(transcript_id: str):
|
||||
pass
|
||||
|
||||
|
||||
@router.websocket("/transcripts/{transcript_id}/events")
|
||||
async def transcript_events_websocket(
|
||||
transcript_id: str,
|
||||
websocket: WebSocket,
|
||||
# user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||
):
|
||||
# user_id = user["sub"] if user else None
|
||||
transcript = await transcripts_controller.get_by_id(transcript_id)
|
||||
if not transcript:
|
||||
raise HTTPException(status_code=404, detail="Transcript not found")
|
||||
|
||||
# connect to websocket manager
|
||||
# use ts:transcript_id as room id
|
||||
room_id = f"ts:{transcript_id}"
|
||||
ws_manager = get_ws_manager()
|
||||
await ws_manager.add_user_to_room(room_id, websocket)
|
||||
|
||||
try:
|
||||
# on first connection, send all events only to the current user
|
||||
for event in transcript.events:
|
||||
# for now, do not send TRANSCRIPT or STATUS options - theses are live event
|
||||
# not necessary to be sent to the client; but keep the rest
|
||||
name = event.event
|
||||
if name in ("TRANSCRIPT", "STATUS"):
|
||||
continue
|
||||
await websocket.send_json(event.model_dump(mode="json"))
|
||||
|
||||
# XXX if transcript is final (locked=True and status=ended)
|
||||
# XXX send a final event to the client and close the connection
|
||||
|
||||
# endless loop to wait for new events
|
||||
# we do not have command system now,
|
||||
while True:
|
||||
await websocket.receive()
|
||||
except (RuntimeError, WebSocketDisconnect):
|
||||
await ws_manager.remove_user_from_room(room_id, websocket)
|
||||
|
||||
|
||||
# ==============================================================
|
||||
# Web RTC
|
||||
# ==============================================================
|
||||
|
||||
|
||||
@router.post("/transcripts/{transcript_id}/record/webrtc")
|
||||
async def transcript_record_webrtc(
|
||||
transcript_id: str,
|
||||
params: RtcOffer,
|
||||
request: Request,
|
||||
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||
):
|
||||
user_id = user["sub"] if user else None
|
||||
transcript = await transcripts_controller.get_by_id_for_http(
|
||||
transcript_id, user_id=user_id
|
||||
)
|
||||
|
||||
if transcript.locked:
|
||||
raise HTTPException(status_code=400, detail="Transcript is locked")
|
||||
|
||||
# create a pipeline runner
|
||||
from reflector.pipelines.main_live_pipeline import PipelineMainLive
|
||||
|
||||
pipeline_runner = PipelineMainLive(transcript_id=transcript_id)
|
||||
|
||||
# FIXME do not allow multiple recording at the same time
|
||||
return await rtc_offer_base(
|
||||
params,
|
||||
request,
|
||||
pipeline_runner=pipeline_runner,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user