import { useEffect, useMemo, useState } from 'react' import { useSubtitles } from '../hooks/useSubtitles' import { css, cva } from '@/styled-system/css' import { styled } from '@/styled-system/jsx' import { Avatar } from '@/components/Avatar' import { Text } from '@/primitives' import { useRoomContext } from '@livekit/components-react' import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor' import { getParticipantName } from '@/features/rooms/utils/getParticipantName' import { Participant, RoomEvent } from 'livekit-client' import { useSnapshot } from 'valtio' import { accessibilityStore, CAPTION_TEXT_SIZE_OPTIONS, CAPTION_FONT_COLOR_VALUES, CAPTION_BACKGROUND_COLOR_VALUES, type CaptionTextSize, } from '@/stores/accessibility' const FONT_SIZE_CONFIG: Record< CaptionTextSize, { fontSize: string; lineHeight: string } > = { small: { fontSize: '0.875rem', lineHeight: '1.2rem' }, medium: { fontSize: '1.5rem', lineHeight: '1.7rem' }, large: { fontSize: '2.25rem', lineHeight: '2.5rem' }, } const CAPTION_FONT_SIZES = Object.fromEntries( CAPTION_TEXT_SIZE_OPTIONS.map((size) => [size, FONT_SIZE_CONFIG[size]]) ) as Record export interface TranscriptionSegment { id: string text: string language: string startTime?: number endTime: number final: boolean firstReceivedTime: number lastReceivedTime: number } export interface TranscriptionSegmentWithParticipant extends TranscriptionSegment { participant: Participant } export interface TranscriptionRow { id: string participant: Participant segments: TranscriptionSegment[] startTime?: number lastUpdateTime: number lastReceivedTime: number } const useTranscriptionState = () => { const [transcriptionSegments, setTranscriptionSegments] = useState< TranscriptionSegmentWithParticipant[] >([]) const updateTranscriptionSegments = ( segments: TranscriptionSegment[], participant?: Participant ) => { console.log(participant, segments) if (!participant || segments.length === 0) return if (segments.length > 1) { console.warn('Unexpected error more segments') return } const segment = segments[0] setTranscriptionSegments((prevSegments) => { const existingSegmentIds = new Set(prevSegments.map((s) => s.id)) if (existingSegmentIds.has(segment.id)) return prevSegments return [ ...prevSegments, { participant: participant, ...segment, }, ] }) } return { updateTranscriptionSegments, transcriptionSegments, } } const Transcription = ({ row }: { row: TranscriptionRow }) => { const { captionTextSize, captionFontColor, captionBackgroundColor } = useSnapshot(accessibilityStore) const participantColor = getParticipantColor(row.participant) const participantName = getParticipantName(row.participant) const { fontSize, lineHeight } = CAPTION_FONT_SIZES[captionTextSize] const fontColor = CAPTION_FONT_COLOR_VALUES[captionFontColor] const backgroundColor = CAPTION_BACKGROUND_COLOR_VALUES[captionBackgroundColor] const getDisplayText = (row: TranscriptionRow): string => { return row.segments .filter((segment) => segment.text.trim()) .map((segment) => segment.text.trim()) .join(' ') } const displayText = getDisplayText(row) if (!displayText) return null return (
{participantName}

{displayText}

) } const SubtitlesWrapper = styled( 'div', cva({ base: { width: '100%', paddingTop: 'var(--lk-grid-gap)', transition: 'height .5s cubic-bezier(0.4,0,0.2,1) 5ms', }, variants: { areOpen: { true: { height: '12rem', }, false: { height: '0', }, }, }, }) ) export const Subtitles = () => { const { areSubtitlesOpen } = useSubtitles() const room = useRoomContext() const { transcriptionSegments, updateTranscriptionSegments } = useTranscriptionState() useEffect(() => { if (!room) return room.on(RoomEvent.TranscriptionReceived, updateTranscriptionSegments) return () => { room.off(RoomEvent.TranscriptionReceived, updateTranscriptionSegments) } }, [room, updateTranscriptionSegments]) const transcriptionRows = useMemo(() => { if (transcriptionSegments.length === 0) return [] const rows: TranscriptionRow[] = [] let currentRow: TranscriptionRow | null = null for (const segment of transcriptionSegments) { const shouldStartNewRow = !currentRow || currentRow.participant.identity !== segment.participant.identity if (shouldStartNewRow) { currentRow = { id: `${segment.participant.identity}-${segment.firstReceivedTime}`, participant: segment.participant, segments: [segment], startTime: segment.startTime, lastUpdateTime: segment.lastReceivedTime, lastReceivedTime: segment.lastReceivedTime, } rows.push(currentRow) } else if (currentRow) { currentRow.segments.push(segment) currentRow.lastUpdateTime = Math.max( currentRow.lastUpdateTime, segment.lastReceivedTime ) currentRow.lastReceivedTime = Math.max( currentRow.lastReceivedTime, segment.lastReceivedTime ) } } return rows }, [transcriptionSegments]) return (
{transcriptionRows .slice() .reverse() .map((row) => ( ))}
) }