diff --git a/server/migrations/versions/ccd68dc784ff_add_performance_indexes.py b/server/migrations/versions/ccd68dc784ff_add_performance_indexes.py new file mode 100644 index 00000000..ad6d214b --- /dev/null +++ b/server/migrations/versions/ccd68dc784ff_add_performance_indexes.py @@ -0,0 +1,59 @@ +"""add_performance_indexes + +Revision ID: ccd68dc784ff +Revises: 20250618140000 +Create Date: 2025-07-15 11:48:42.854741 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "ccd68dc784ff" +down_revision: Union[str, None] = "20250618140000" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("meeting", schema=None) as batch_op: + batch_op.create_index("idx_meeting_room_id", ["room_id"], unique=False) + + with op.batch_alter_table("recording", schema=None) as batch_op: + batch_op.create_index("idx_recording_meeting_id", ["meeting_id"], unique=False) + + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.create_index("idx_room_is_shared", ["is_shared"], unique=False) + + with op.batch_alter_table("transcript", schema=None) as batch_op: + batch_op.create_index("idx_transcript_created_at", ["created_at"], unique=False) + batch_op.create_index( + "idx_transcript_recording_id", ["recording_id"], unique=False + ) + batch_op.create_index("idx_transcript_user_id", ["user_id"], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("transcript", schema=None) as batch_op: + batch_op.drop_index("idx_transcript_user_id") + batch_op.drop_index("idx_transcript_recording_id") + batch_op.drop_index("idx_transcript_created_at") + + with op.batch_alter_table("room", schema=None) as batch_op: + batch_op.drop_index("idx_room_is_shared") + + with op.batch_alter_table("recording", schema=None) as batch_op: + batch_op.drop_index("idx_recording_meeting_id") + + with op.batch_alter_table("meeting", schema=None) as batch_op: + batch_op.drop_index("idx_meeting_room_id") + + # ### end Alembic commands ### diff --git a/server/reflector/db/meetings.py b/server/reflector/db/meetings.py index 8117b3ed..48582497 100644 --- a/server/reflector/db/meetings.py +++ b/server/reflector/db/meetings.py @@ -40,6 +40,7 @@ meetings = sa.Table( nullable=False, server_default=sa.true(), ), + sa.Index("idx_meeting_room_id", "room_id"), ) meeting_consent = sa.Table( diff --git a/server/reflector/db/recordings.py b/server/reflector/db/recordings.py index 31670609..15e769ca 100644 --- a/server/reflector/db/recordings.py +++ b/server/reflector/db/recordings.py @@ -20,6 +20,7 @@ recordings = sa.Table( server_default="pending", ), sa.Column("meeting_id", sa.String), + sa.Index("idx_recording_meeting_id", "meeting_id"), ) diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py index 27837eb1..6e98acff 100644 --- a/server/reflector/db/rooms.py +++ b/server/reflector/db/rooms.py @@ -39,6 +39,7 @@ rooms = sqlalchemy.Table( sqlalchemy.Column( "is_shared", sqlalchemy.Boolean, nullable=False, server_default=false() ), + sqlalchemy.Index("idx_room_is_shared", "is_shared"), ) diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index 67f66aac..85d4bbb2 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -6,7 +6,6 @@ from contextlib import asynccontextmanager from datetime import datetime from pathlib import Path from typing import Any, Literal -from reflector.utils import generate_uuid4 import sqlalchemy from fastapi import HTTPException @@ -15,6 +14,7 @@ from reflector.db import database, metadata from reflector.processors.types import Word as ProcessorWord from reflector.settings import settings from reflector.storage import get_transcripts_storage +from reflector.utils import generate_uuid4 from sqlalchemy import Enum from sqlalchemy.sql import false, or_ @@ -74,6 +74,9 @@ transcripts = sqlalchemy.Table( # the main "audio deleted" is the presence of the audio itself / consents not-given # same field could've been in recording/meeting, and it's maybe even ok to dupe it at need sqlalchemy.Column("audio_deleted", sqlalchemy.Boolean, nullable=True), + sqlalchemy.Index("idx_transcript_recording_id", "recording_id"), + sqlalchemy.Index("idx_transcript_user_id", "user_id"), + sqlalchemy.Index("idx_transcript_created_at", "created_at"), ) @@ -306,6 +309,7 @@ class TranscriptController: room_id: str | None = None, search_term: str | None = None, return_query: bool = False, + exclude_columns: list[str] = ["topics", "events", "participants"], ) -> list[Transcript]: """ Get all transcripts @@ -348,9 +352,14 @@ class TranscriptController: if search_term: query = query.where(transcripts.c.title.ilike(f"%{search_term}%")) + # Exclude heavy JSON columns from list queries + transcript_columns = [ + col for col in transcripts.c if col.name not in exclude_columns + ] + query = query.with_only_columns( - [ - transcripts, + transcript_columns + + [ rooms.c.id.label("room_id"), rooms.c.name.label("room_name"), ] diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 27d6188e..19c273c3 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -45,7 +45,7 @@ def create_access_token(data: dict, expires_delta: timedelta): # ============================================================== -class GetTranscript(BaseModel): +class GetTranscriptMinimal(BaseModel): id: str user_id: str | None name: str @@ -59,7 +59,6 @@ class GetTranscript(BaseModel): share_mode: str = Field("private") source_language: str | None target_language: str | None - participants: list[TranscriptParticipant] | None reviewed: bool meeting_id: str | None source_kind: SourceKind @@ -68,6 +67,10 @@ class GetTranscript(BaseModel): audio_deleted: bool | None = None +class GetTranscript(GetTranscriptMinimal): + participants: list[TranscriptParticipant] | None + + class CreateTranscript(BaseModel): name: str source_language: str = Field("en") @@ -90,7 +93,7 @@ class DeletionStatus(BaseModel): status: str -@router.get("/transcripts", response_model=Page[GetTranscript]) +@router.get("/transcripts", response_model=Page[GetTranscriptMinimal]) async def transcripts_list( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], source_kind: SourceKind | None = None, diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index 70bd38df..4a7de7b7 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -44,7 +44,7 @@ import { import useTranscriptList from "../transcripts/useTranscriptList"; import useSessionUser from "../../lib/useSessionUser"; import NextLink from "next/link"; -import { Room, GetTranscript } from "../../api"; +import { Room, GetTranscriptMinimal } from "../../api"; import Pagination from "./pagination"; import { formatTimeMs } from "../../lib/time"; import useApi from "../../lib/useApi"; @@ -328,7 +328,7 @@ export default function TranscriptBrowser() { - {response?.items?.map((item: GetTranscript) => ( + {response?.items?.map((item: GetTranscriptMinimal) => ( @@ -416,7 +416,7 @@ export default function TranscriptBrowser() { - {response?.items?.map((item: GetTranscript) => ( + {response?.items?.map((item: GetTranscriptMinimal) => ( diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index ab881eea..e8334337 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -193,7 +193,7 @@ export default function RoomsList() { (err.body as any).detail == "Room name is not unique" ) { setNameError( - "This room name is already taken. Please choose a different name." + "This room name is already taken. Please choose a different name.", ); } else { setNameError("An error occurred. Please try again."); @@ -316,7 +316,7 @@ export default function RoomsList() { options={roomModeOptions} value={{ label: roomModeOptions.find( - (rm) => rm.value === room.roomMode + (rm) => rm.value === room.roomMode, )?.label, value: room.roomMode, }} @@ -335,7 +335,7 @@ export default function RoomsList() { options={recordingTypeOptions} value={{ label: recordingTypeOptions.find( - (rt) => rt.value === room.recordingType + (rt) => rt.value === room.recordingType, )?.label, value: room.recordingType, }} @@ -358,7 +358,7 @@ export default function RoomsList() { options={recordingTriggerOptions} value={{ label: recordingTriggerOptions.find( - (rt) => rt.value === room.recordingTrigger + (rt) => rt.value === room.recordingTrigger, )?.label, value: room.recordingTrigger, }} diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/topicPlayer.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/topicPlayer.tsx index 3d07e3d9..1b769fe2 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/topicPlayer.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/topicPlayer.tsx @@ -183,17 +183,21 @@ const TopicPlayer = ({ setIsPlaying(false); }; - const isLoaded = !mp3.loading && !!topicTime + const isLoaded = !mp3.loading && !!topicTime; const error = mp3.error; if (error !== null) { - return - Loading error: {error} - + return ( + + Loading error: {error} + + ); } if (mp3.audioDeleted) { - return - This topic file has been deleted. - + return ( + + This topic file has been deleted. + + ); } return ( - {waveform.waveform && mp3.media && !mp3.audioDeleted && topics.topics ? ( + {waveform.waveform && + mp3.media && + !mp3.audioDeleted && + topics.topics ? ( { - - const isClient = typeof window !== 'undefined'; + const isClient = typeof window !== "undefined"; const router = useRouter(); const { isLoading, isAuthenticated } = useSessionStatus(); const requireLogin = featureEnabled("requireLogin"); @@ -186,9 +185,9 @@ const TranscriptCreate = () => { ) : permissionDenied ? ( - Permission to use your microphone was denied, please change - the permission setting in your browser and refresh this - page. + Permission to use your microphone was denied, please + change the permission setting in your browser and refresh + this page. ) : (