Spaces:
Running
on
A100
Running
on
A100
| import {useRef, useEffect, useCallback} from "react"; | |
| import {AlignedSegment} from "../services/transcriptionApi"; | |
| import {useTranscriptionStore} from "../stores/transcriptionStore"; | |
| import {useMediaTimeSync} from "../hooks/useMediaTimeSync"; | |
| import { | |
| SUPPORTED_AUDIO_FORMATS, | |
| SUPPORTED_VIDEO_FORMATS, | |
| CODEC_INFO, | |
| } from "../utils/mediaTypes"; | |
| import MediaPlayer from "./MediaPlayer"; | |
| import CanvasTimeline from "./CanvasTimeline"; | |
| import MinimapTimeline from "./MinimapTimeline"; | |
| import TranscriptionControls from "./TranscriptionControls"; | |
| import FullTranscription from "./FullTranscription"; | |
| import ErrorBoundary from "./ErrorBoundary"; | |
| export default function TranscriptionPlayer() { | |
| // Get state from store | |
| const {file, mediaUrl, transcription, isLoading, isProcessingVideo} = | |
| useTranscriptionStore(); | |
| // Get actions from store | |
| const { | |
| handleTimeUpdate: updateTimeInStore, | |
| setSelectedSegmentIndex, | |
| selectedSegmentIndex, | |
| setMediaRefs, | |
| } = useTranscriptionStore(); | |
| const audioRef = useRef<HTMLAudioElement>(null); | |
| const videoRef = useRef<HTMLVideoElement>(null); | |
| const canvasTimelineRef = useRef<HTMLDivElement>(null); | |
| // Set media refs in store for centralized seeking | |
| useEffect(() => { | |
| setMediaRefs(audioRef, videoRef); | |
| }, [setMediaRefs]); | |
| const handleTimeUpdate = useCallback(() => { | |
| const mediaElement = audioRef.current || videoRef.current; | |
| if (mediaElement && transcription?.aligned_segments) { | |
| const mediaCurrentTime = mediaElement.currentTime; | |
| // Find the active segment with a small tolerance for timing precision | |
| const activeIndex = transcription.aligned_segments.findIndex( | |
| (segment) => | |
| mediaCurrentTime >= segment.start && mediaCurrentTime <= segment.end | |
| ); | |
| // If no exact match, find the closest segment | |
| let finalActiveIndex: number | null = activeIndex; | |
| if (activeIndex === -1) { | |
| let closestIndex = -1; | |
| let minDistance = Infinity; | |
| transcription.aligned_segments.forEach((segment, index) => { | |
| const distance = Math.min( | |
| Math.abs(mediaCurrentTime - segment.start), | |
| Math.abs(mediaCurrentTime - segment.end) | |
| ); | |
| if (distance < minDistance && distance < 0.5) { | |
| // 0.5 second tolerance | |
| minDistance = distance; | |
| closestIndex = index; | |
| } | |
| }); | |
| finalActiveIndex = closestIndex >= 0 ? closestIndex : null; | |
| } | |
| updateTimeInStore(); | |
| // Auto-select the active segment only if: | |
| // 1. We found an active segment | |
| // 2. Either no segment is selected, or the active segment changed | |
| if ( | |
| finalActiveIndex !== null && | |
| selectedSegmentIndex !== finalActiveIndex | |
| ) { | |
| setSelectedSegmentIndex(finalActiveIndex); | |
| } | |
| } | |
| }, [ | |
| transcription, | |
| updateTimeInStore, | |
| selectedSegmentIndex, | |
| setSelectedSegmentIndex, | |
| ]); | |
| const handleSeekToSegment = (segment: AlignedSegment) => { | |
| const mediaElement = audioRef.current || videoRef.current; | |
| if (mediaElement) { | |
| mediaElement.currentTime = segment.start; | |
| // Immediately update the store to sync the progress indicator | |
| handleTimeUpdate(); | |
| } | |
| }; | |
| // Use media time sync hook for continuous time updates during playback | |
| useMediaTimeSync({ | |
| audioRef, | |
| videoRef, | |
| onTimeUpdate: handleTimeUpdate, | |
| transcription, | |
| }); | |
| // Cleanup media URL on unmount | |
| useEffect(() => { | |
| return () => { | |
| if (mediaUrl) { | |
| URL.revokeObjectURL(mediaUrl); | |
| } | |
| }; | |
| }, [mediaUrl]); | |
| return ( | |
| <div className="flex-1 min-w-0 flex flex-col bg-black"> | |
| {/* Media Player */} | |
| {file && ( | |
| <ErrorBoundary componentName="MediaPlayer"> | |
| <MediaPlayer | |
| audioRef={audioRef} | |
| videoRef={videoRef} | |
| onTimeUpdate={handleTimeUpdate} | |
| /> | |
| </ErrorBoundary> | |
| )} | |
| {/* Transcription Controls */} | |
| <ErrorBoundary componentName="TranscriptionControls"> | |
| <TranscriptionControls /> | |
| </ErrorBoundary> | |
| {/* Full Transcription */} | |
| <ErrorBoundary componentName="FullTranscription"> | |
| <FullTranscription /> | |
| </ErrorBoundary> | |
| {/* Transcription Timeline */} | |
| {transcription && ( | |
| <> | |
| {/* Minimap Timeline */} | |
| <ErrorBoundary componentName="MinimapTimeline"> | |
| <MinimapTimeline | |
| audioRef={audioRef} | |
| videoRef={videoRef} | |
| canvasTimelineRef={canvasTimelineRef} | |
| /> | |
| </ErrorBoundary> | |
| {/* Canvas Timeline */} | |
| {/* <ErrorBoundary componentName="CanvasTimeline"> | |
| <CanvasTimeline | |
| audioRef={audioRef} | |
| videoRef={videoRef} | |
| onSeekToSegment={handleSeekToSegment} | |
| onTimeUpdate={handleTimeUpdate} | |
| ref={canvasTimelineRef} | |
| /> | |
| </ErrorBoundary> */} | |
| </> | |
| )} | |
| {/* Transcription Loading State */} | |
| {file && !transcription && (isLoading || isProcessingVideo) && ( | |
| <div className="flex-1 flex items-center justify-center bg-gray-900 border-t border-gray-700"> | |
| <div className="text-center text-white"> | |
| <div className="mb-4"> | |
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div> | |
| </div> | |
| <div className="text-2xl md:text-3xl mb-3 font-semibold"> | |
| {file?.type.startsWith("video/") | |
| ? "Processing Video..." | |
| : "Transcribing Audio..."} | |
| </div> | |
| <div className="text-base md:text-lg text-gray-400 max-w-md mx-auto"> | |
| {file?.type.startsWith("video/") | |
| ? "Server is extracting audio and generating transcription" | |
| : "Converting speech to text"} | |
| {/* : "Converting speech to text with timestamps"} */} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* No File State */} | |
| {!file && ( | |
| <div className="flex-1 flex items-center justify-center"> | |
| <div className="text-center text-gray-400"> | |
| <div className="text-6xl mb-4">🎵</div> | |
| <div className="text-xl mb-2">Upload Audio</div> | |
| <div className="text-sm mb-4"> | |
| Choose an audio file or drag and drop or record audio from the | |
| panel on the left anywhere to get started with transcription | |
| </div> | |
| {/* Supported File Types */} | |
| <div className="text-xs text-gray-500 max-w-md mx-auto"> | |
| {/* Audio formats section */} | |
| <div className="text-center mb-3"> | |
| <div className="font-medium text-gray-400 mb-1"> | |
| Audio Formats | |
| </div> | |
| <div className="text-xs text-gray-500"> | |
| {SUPPORTED_AUDIO_FORMATS.join(" • ")} | |
| </div> | |
| </div> | |
| {/* Codec info */} | |
| <div className="text-center"> | |
| <div className="text-xs text-gray-400 opacity-75"> | |
| Recommended: {CODEC_INFO.audio.common.slice(0, 2).join(", ")}{" "} | |
| codecs | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |