open source transcript player

Open-Source Transcript Player

React-based video player with a synchronized transcript and editor for notes, or AI summaries.

Overview

This React application provides a simple video player with a synchronized transcript display. Key features include:
  • Video playback with standard controls
  • Synchronized transcript display
  • Current word highlighting in the transcript
  • Video navigation by clicking on transcript words
  • Responsive interface with resizable split view

Data is loaded from a mock JSON file, which matches the typical format of the transcript data returned by a transcription API, in this case the /GET route of Meeting Baas.

The project is under the MIT license, free to use and modify it.

For a full-featured example, including a back-end which stores the data, check out the Transcript Seeker repository.
open source transcript player interface

Technologies Used

Main Components

  • App.tsx: Manages global state and layout
  • VideoPlayer.tsx: Wrapper for video.js
  • Transcript.tsx: Displays synchronized transcript
  • Card.tsx: Reusable component for content display
View on GitHub
import { Box, Center, Flex, Text } from '@chakra-ui/react' import React, { useCallback, useEffect, useState } from 'react' import Player from 'video.js/dist/types/player' import { Card } from './components/Card' import Editor from './components/Editor' import Transcript from './components/Transcript' import { VideoPlayer } from './components/VideoPlayer' import videoExample from './fakeData/customer_service_480x360.mp4' import jsonData from './fakeData/fakedata.json' import { VideoData } from './type' const videoData: VideoData = jsonData as VideoData const App: React.FC = () => { const [isHover, setIsHover] = useState(false) const [width, setWidth] = useState(30) const [isDragging, setIsDragging] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [videoPlayer, setVideoPlayer] = useState<Player | null>(null) const [editorData, setEditorData] = useState({}) const handleEditorChange = (data: any) => { setEditorData(data) } const handleMouseDown = useCallback(() => { setIsDragging(true) }, []) const handleMouseMove = useCallback( (e: MouseEvent) => { if (isDragging) { let newWidth = (e.clientX / window.innerWidth) * 100 newWidth = Math.min(Math.max(newWidth, 35), 65) setWidth(newWidth) } }, [isDragging] ) const handleMouseUp = useCallback(() => { setIsDragging(false) }, []) useEffect(() => { window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) return () => { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) } }, [handleMouseMove, handleMouseUp]) const handleTimeUpdate = useCallback((time: number) => { setCurrentTime(time) }, []) const handleSeek = useCallback( (time: number) => { if (videoPlayer) { videoPlayer.currentTime(time) } }, [videoPlayer] ) const setPlayerRef = useCallback((player: Player) => { setVideoPlayer(player) }, []) return ( <Flex h="100vh" w="100vw" bg="gray.100" gap="4" p="4" flexDir={{ base: 'column', lg: 'row' }} > <Flex flexDir={'column'} h="full" minW={{ base: 'full', lg: `${width}%` }} w={{ base: 'full', lg: `${width}%` }} gap="4" > <VideoPlayer url={videoExample} onTimeUpdate={handleTimeUpdate} setPlayerRef={setPlayerRef} /> <Card header={ <Text size="md" fontWeight={'semibold'}> Transcript </Text> } title="Transcript" isOpen={true} toggleCard={() => {}} bodyContent={ <Transcript transcript={videoData.data.transcript} currentTime={currentTime} onWordClick={handleSeek} /> } /> </Flex> <Center display={{ base: 'none', lg: 'flex' }} onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} px="2px" h="full" gap="2px" cursor="ew-resize" onMouseDown={handleMouseDown} opacity={isHover || isDragging ? '1' : '0'} > <Box h="50px" cursor="ew-resize" w="4px" bg="black" rounded="full" /> <Box h="50px" cursor="ew-resize" w="4px" bg="black" rounded="full" /> </Center> <Card header={ <Text size="md" fontWeight={'semibold'}> Editor </Text> } title="Editor" isOpen={true} toggleCard={() => {}} bodyContent={ <Editor data={editorData} onChange={handleEditorChange} /> } /> </Flex> ) } export default App
Here's a simple script that clones the code repo and launches the example:
View on GitHub
#!/bin/bash # 🚀 Quick Start for Video Player with Synchronized Transcript 🚀 # Save this file as run.sh, chmod +x run.sh, and run it # ⚠️ Make sure you have Git, Node.js, and Yarn installed before proceeding! # Clone the repository echo "📥 Cloning the repository..." git clone https://github.com/Meeting-Baas/meeting-bot-as-a-service && cd meeting-bot-as-a-service || { echo "❌ Error: Failed to clone repository"; exit 1; } # Navigate to the player interface directory echo "📂 Navigating to player interface directory..." cd apps/player-interface || { echo "❌ Error: Failed to navigate to player interface directory"; exit 2; } # Install dependencies echo "🔧 Installing dependencies..." yarn install || { echo "❌ Error: Failed to install dependencies"; exit 3; } # Run the application echo "🚀 Starting the development server..." yarn dev || { echo "❌ Error: Failed to start the development server"; exit 4; } echo "✅ Setup complete! The application should now be running on your local development server at http://localhost:3000"

Usage

The application loads a video and its transcript from mock data. Users can:

  • Play the video and see synchronized transcript
  • Click on words in the transcript to navigate the video
  • Resize the split between video and transcript on larger screens

Customization

The data structure in fakedata.json aligns with the Meeting Baas API /GET route format. This structure is compatible with various transcription API outputs, facilitating integration with different services.

The Transcript component processes this data, rendering speaker information and individual words. It tracks the current playback time, highlighting the corresponding word and enabling navigation on word click.

View on GitHub
// types.ts export interface Word { start: number end: number word: string } export interface TranscriptEntry { speaker: string words: Word[] } export interface VideoData { event: string data: { bot_id: string transcript: TranscriptEntry[] speakers: string[] mp4: string } }
preview features