import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { FileSelector } from './components/FileSelector'; import { GeminiSettings, GeminiConfig } from './components/GeminiSettings'; import { VideoTimeline } from './components/VideoTimeline'; import './styles/index.css'; interface FileState { path: string; name: string; } // Must match the Rust TranscriptionResult struct exactly interface TranscriptionState { visualDescription: string; // from visual_description in Rust subtitleNarrative: string; // from subtitle_narrative in Rust } interface Thumbnail { path: string; time: number; } function App() { const [videoFile, setVideoFile] = useState(null); const [subtitleFile, setSubtitleFile] = useState(null); const [progress, setProgress] = useState(0); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(null); const [addBlackBar, setAddBlackBar] = useState(true); const [resizeTo720p, setResizeTo720p] = useState(true); const [geminiConfig, setGeminiConfig] = useState({ enabled: false, apiKey: 'AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc', isDefault: true }); const [selectedTime, setSelectedTime] = useState(10); const [transcription, setTranscription] = useState(null); const [thumbnails, setThumbnails] = useState([]); useEffect(() => { invoke('check_dependencies') .catch(err => { setError(err as string); console.error('Dependency check failed:', err); }); }, []); const handleVideoSelect = (path: string) => { const name = path.split('/').pop() || ''; setVideoFile({ path, name }); setThumbnails([]); // Clear thumbnails when new video is selected }; const handleSubtitleSelect = (path: string) => { const name = path.split('/').pop() || ''; setSubtitleFile({ path, name }); }; const handleThumbnailsGenerated = (newThumbnails: Thumbnail[]) => { setThumbnails(newThumbnails); }; useEffect(() => { let intervalId: number | undefined; if (isProcessing) { // Poll for progress every 100ms while processing intervalId = window.setInterval(async () => { try { const currentProgress = await invoke('get_progress'); console.log('Progress update:', currentProgress); setProgress(currentProgress); } catch (error) { console.error('Failed to get progress:', error); } }, 100); } return () => { if (intervalId !== undefined) { window.clearInterval(intervalId); } }; }, [isProcessing]); const handleMerge = async () => { if (!videoFile || !subtitleFile) return; setIsProcessing(true); setProgress(0); setTranscription(null); try { console.log('Starting merge with args:', { videoPath: videoFile.path, subtitlePath: subtitleFile.path, addBlackBar, resizeTo720p, geminiEnabled: geminiConfig.enabled, frameTime: selectedTime }); // Process transcription first if Gemini is enabled if (geminiConfig.enabled) { try { console.log('Starting transcription process...'); const transcriptionResult = await invoke('process_transcription', { args: { video_path: videoFile.path, subtitle_path: subtitleFile.path, frame_time: selectedTime, api_key: geminiConfig.apiKey } }); console.log('Transcription completed with full response:', transcriptionResult); // Log the raw response first console.log('Raw transcription result:', JSON.stringify(transcriptionResult, null, 2)); // Validate the response structure if (!transcriptionResult || typeof transcriptionResult !== 'object') { console.error('Invalid transcription result:', transcriptionResult); setError('Invalid transcription response format'); return; } // Validate and clean the content const visualDescription = String(transcriptionResult.visualDescription || '').trim(); const subtitleNarrative = String(transcriptionResult.subtitleNarrative || '').trim(); if (!visualDescription || !subtitleNarrative) { console.error('Missing transcription content:', { visualDescription: !!visualDescription, subtitleNarrative: !!subtitleNarrative }); setError('Transcription response is missing required content'); return; } // Create a new state object with the validated content const newTranscription: TranscriptionState = { visualDescription, subtitleNarrative }; console.log('Setting transcription state:', { visualDescriptionLength: visualDescription.length, subtitleNarrativeLength: subtitleNarrative.length }); setTranscription(newTranscription); } catch (error) { console.error('Transcription error:', error); setError(`Transcription error: ${error}`); return; } } // Then start video merging process const mergeResult = await invoke('merge_video_subtitle', { args: { video_path: videoFile.path, subtitle_path: subtitleFile.path, add_black_bar: addBlackBar, resize_to_720p: resizeTo720p } }); console.log('Merge completed:', mergeResult); setProgress(100); } catch (error) { console.error('Error merging files:', error); setError(error as string); } finally { setIsProcessing(false); } }; return (

Video Subtitle Merger 2.0

Video file:

{videoFile?.name || 'No file selected'}

Subtitle file:

{subtitleFile?.name || 'No file selected'}

{geminiConfig.enabled && videoFile && ( )}
{transcription ? (

DeafBlind Accessibility Transcription

Visual Description

{transcription.visualDescription}

Subtitle Narrative

{transcription.subtitleNarrative}

) : null}

{isProcessing ? `${Math.round(progress)}%` : ''}

{error && (
Error: {error}
)}
); } export default App;