diff --git a/server/migrations/versions/74b2b0236931_add_transcript_source_kind.py b/server/migrations/versions/74b2b0236931_add_transcript_source_kind.py new file mode 100644 index 00000000..3c4c5600 --- /dev/null +++ b/server/migrations/versions/74b2b0236931_add_transcript_source_kind.py @@ -0,0 +1,48 @@ +"""Add transcript source kind + +Revision ID: 74b2b0236931 +Revises: 0925da921477 +Create Date: 2024-10-04 14:19:23.625447 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "74b2b0236931" +down_revision: Union[str, None] = "0925da921477" +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! ### + op.add_column( + "transcript", + sa.Column( + "source_kind", + sa.Enum("ROOM", "LIVE", "FILE", name="sourcekind"), + nullable=True, + ), + ) + + op.execute( + "UPDATE transcript SET source_kind = 'room' WHERE meeting_id IS NOT NULL" + ) + op.execute("UPDATE transcript SET source_kind = 'live' WHERE meeting_id IS NULL") + + with op.batch_alter_table("transcript", schema=None) as batch_op: + batch_op.alter_column("source_kind", nullable=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("transcript", "source_kind") + + +# ### end Alembic commands ### diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index d9a9eaf9..2459b239 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -1,3 +1,4 @@ +import enum import json import os import shutil @@ -14,8 +15,16 @@ from reflector.db import database, metadata from reflector.processors.types import Word as ProcessorWord from reflector.settings import settings from reflector.storage import Storage +from sqlalchemy import Enum from sqlalchemy.sql import false + +class SourceKind(enum.StrEnum): + ROOM = enum.auto() + LIVE = enum.auto() + FILE = enum.auto() + + transcripts = sqlalchemy.Table( "transcript", metadata, @@ -55,6 +64,11 @@ transcripts = sqlalchemy.Table( sqlalchemy.String, ), sqlalchemy.Column("zulip_message_id", sqlalchemy.Integer, nullable=True), + sqlalchemy.Column( + "source_kind", + Enum(SourceKind, values_callable=lambda obj: [e.value for e in obj]), + nullable=False, + ), ) @@ -152,6 +166,7 @@ class Transcript(BaseModel): reviewed: bool = False meeting_id: str | None = None zulip_message_id: int | None = None + source_kind: SourceKind def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: ev = TranscriptEvent(event=event, data=data.model_dump()) @@ -291,6 +306,7 @@ class TranscriptController: order_by: str | None = None, filter_empty: bool | None = False, filter_recording: bool | None = False, + source_kind: SourceKind | None = None, room_id: str | None = None, search_term: str | None = None, return_query: bool = False, @@ -320,6 +336,9 @@ class TranscriptController: if user_id: query = query.where(transcripts.c.user_id == user_id) + if source_kind: + query = query.where(transcripts.c.source_kind == source_kind) + if room_id: query = query.where(rooms.c.id == room_id) @@ -422,6 +441,7 @@ class TranscriptController: async def add( self, name: str, + source_kind: SourceKind, source_language: str = "en", target_language: str = "en", user_id: str | None = None, @@ -433,6 +453,7 @@ class TranscriptController: """ transcript = Transcript( name=name, + source_kind=source_kind, source_language=source_language, target_language=target_language, user_id=user_id, diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index cd531889..05d7bfb0 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -9,6 +9,7 @@ from jose import jwt from pydantic import BaseModel, Field from reflector.db.migrate_user import migrate_user from reflector.db.transcripts import ( + SourceKind, TranscriptParticipant, TranscriptTopic, transcripts_controller, @@ -61,6 +62,7 @@ class GetTranscript(BaseModel): meeting_id: str | None room_id: str | None room_name: str | None + source_kind: SourceKind class CreateTranscript(BaseModel): @@ -89,6 +91,7 @@ async def transcripts_list( room_id: str | None, search_term: str | None, user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], + source_kind: SourceKind | None = None, ): from reflector.db import database @@ -105,6 +108,7 @@ async def transcripts_list( database, await transcripts_controller.get_all( user_id=user_id, + source_kind=SourceKind(source_kind) if source_kind else None, room_id=room_id, search_term=search_term, order_by="-created_at", @@ -121,6 +125,7 @@ async def transcripts_create( user_id = user["sub"] if user else None return await transcripts_controller.add( info.name, + source_kind=SourceKind.LIVE, source_language=info.source_language, target_language=info.target_language, user_id=user_id, diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index 614f7c4c..fba09996 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -8,7 +8,7 @@ import structlog from celery import shared_task from celery.utils.log import get_task_logger from reflector.db.meetings import meetings_controller -from reflector.db.transcripts import transcripts_controller +from reflector.db.transcripts import SourceKind, transcripts_controller from reflector.pipelines.main_live_pipeline import asynctask, task_pipeline_process from reflector.settings import settings @@ -66,6 +66,7 @@ async def process_recording(bucket_name: str, object_key: str): meeting = await meetings_controller.get_by_room_name(room_name) transcript = await transcripts_controller.add( "", + source_kind=SourceKind.ROOM source_language="en", target_language="en", user_id=meeting.user_id, diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index bc9cad09..bc1a1254 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -49,8 +49,11 @@ import Pagination from "./pagination"; import { formatTimeMs } from "../../lib/time"; import useApi from "../../lib/useApi"; import { useError } from "../../(errors)/errorContext"; +import { SourceKind } from "../../api"; export default function TranscriptBrowser() { + const [selectedSourceKind, setSelectedSourceKind] = + useState(null); const [selectedRoomId, setSelectedRoomId] = useState(""); const [rooms, setRooms] = useState([]); const [page, setPage] = useState(1); @@ -58,6 +61,7 @@ export default function TranscriptBrowser() { const [searchInputValue, setSearchInputValue] = useState(""); const { loading, response, refetch } = useTranscriptList( page, + selectedSourceKind, selectedRoomId, searchTerm, ); @@ -86,7 +90,11 @@ export default function TranscriptBrowser() { .catch((err) => setError(err, "There was an error fetching the rooms")); }, [api]); - const handleFilterTranscripts = (roomId: string) => { + const handleFilterTranscripts = ( + sourceKind: SourceKind | null, + roomId: string, + ) => { + setSelectedSourceKind(sourceKind); setSelectedRoomId(roomId); setPage(1); }; @@ -187,10 +195,10 @@ export default function TranscriptBrowser() { handleFilterTranscripts("")} - color={selectedRoomId === "" ? "blue.500" : "gray.600"} + onClick={() => handleFilterTranscripts(null, "")} + color={selectedSourceKind === null ? "blue.500" : "gray.600"} _hover={{ color: "blue.300" }} - fontWeight={selectedRoomId === "" ? "bold" : "normal"} + fontWeight={selectedSourceKind === null ? "bold" : "normal"} > All Transcripts @@ -208,10 +216,20 @@ export default function TranscriptBrowser() { key={room.id} as={NextLink} href="#" - onClick={() => handleFilterTranscripts(room.id)} - color={selectedRoomId === room.id ? "blue.500" : "gray.600"} + onClick={() => handleFilterTranscripts("room", room.id)} + color={ + selectedSourceKind === "room" && + selectedRoomId === room.id + ? "blue.500" + : "gray.600" + } _hover={{ color: "blue.300" }} - fontWeight={selectedRoomId === room.id ? "bold" : "normal"} + fontWeight={ + selectedSourceKind === "room" && + selectedRoomId === room.id + ? "bold" + : "normal" + } ml={4} > {room.name} @@ -219,6 +237,28 @@ export default function TranscriptBrowser() { ))} )} + + + handleFilterTranscripts("live", "")} + color={selectedSourceKind === "live" ? "blue.500" : "gray.600"} + _hover={{ color: "blue.300" }} + fontWeight={selectedSourceKind === "live" ? "bold" : "normal"} + > + Live Transcripts + + handleFilterTranscripts("file", "")} + color={selectedSourceKind === "file" ? "blue.500" : "gray.600"} + _hover={{ color: "blue.300" }} + fontWeight={selectedSourceKind === "file" ? "bold" : "normal"} + > + Uploaded Files + @@ -241,7 +281,7 @@ export default function TranscriptBrowser() { Transcription Title - Room + Source Date Duration @@ -296,7 +336,11 @@ export default function TranscriptBrowser() { - {item.room_name} + + {item.source_kind === "room" + ? item.room_name + : item.source_kind} + {new Date(item.created_at).toLocaleString("en-US", { year: "numeric", @@ -376,7 +420,12 @@ export default function TranscriptBrowser() { {item.title || "Unnamed Transcript"} - Room: {item.room_name} + + Source:{" "} + {item.source_kind === "room" + ? item.room_name + : item.source_kind} + Date: {new Date(item.created_at).toLocaleString()} diff --git a/www/app/(app)/transcripts/useTranscriptList.ts b/www/app/(app)/transcripts/useTranscriptList.ts index ef61b8ba..e7bf2dd0 100644 --- a/www/app/(app)/transcripts/useTranscriptList.ts +++ b/www/app/(app)/transcripts/useTranscriptList.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useError } from "../../(errors)/errorContext"; import useApi from "../../lib/useApi"; -import { Page_GetTranscript_ } from "../../api"; +import { Page_GetTranscript_, SourceKind } from "../../api"; type TranscriptList = { response: Page_GetTranscript_ | null; @@ -12,6 +12,7 @@ type TranscriptList = { const useTranscriptList = ( page: number, + sourceKind: SourceKind | null, roomId: string | null, searchTerm: string | null, ): TranscriptList => { @@ -31,7 +32,12 @@ const useTranscriptList = ( if (!api) return; setLoading(true); api - .v1TranscriptsList({ page, roomId, searchTerm }) + .v1TranscriptsList({ + page, + sourceKind, + roomId, + searchTerm, + }) .then((response) => { setResponse(response); setLoading(false); diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index ec34c9e4..1e11600c 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -285,6 +285,9 @@ export const $GetTranscript = { ], title: "Room Name", }, + source_kind: { + $ref: "#/components/schemas/SourceKind", + }, }, type: "object", required: [ @@ -305,6 +308,7 @@ export const $GetTranscript = { "meeting_id", "room_id", "room_name", + "source_kind", ], title: "GetTranscript", } as const; @@ -766,6 +770,12 @@ export const $RtcOffer = { title: "RtcOffer", } as const; +export const $SourceKind = { + type: "string", + enum: ["room", "live", "file"], + title: "SourceKind", +} as const; + export const $SpeakerAssignment = { properties: { speaker: { diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index 9ddc1b8c..f3e07a30 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -201,6 +201,7 @@ export class DefaultService { * @param data The data for the request. * @param data.roomId * @param data.searchTerm + * @param data.sourceKind * @param data.page Page number * @param data.size Page size * @returns Page_GetTranscript_ Successful Response @@ -215,6 +216,7 @@ export class DefaultService { query: { room_id: data.roomId, search_term: data.searchTerm, + source_kind: data.sourceKind, page: data.page, size: data.size, }, diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index ffdf5e4b..de3167cb 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -54,6 +54,7 @@ export type GetTranscript = { meeting_id: string | null; room_id: string | null; room_name: string | null; + source_kind: SourceKind; }; export type GetTranscriptSegmentTopic = { @@ -149,6 +150,8 @@ export type RtcOffer = { type: string; }; +export type SourceKind = "room" | "live" | "file"; + export type SpeakerAssignment = { speaker?: number | null; participant?: string | null; @@ -282,6 +285,7 @@ export type V1TranscriptsListData = { * Page size */ size?: number; + sourceKind?: SourceKind | null; }; export type V1TranscriptsListResponse = Page_GetTranscript_;