Fix temp dir for thumbnails
This commit is contained in:
parent
2b44625655
commit
8639e5cdd6
9 changed files with 204 additions and 93 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "subtitle-merge",
|
"name": "deafblind-video-assistant",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use std::io::BufRead;
|
||||||
use tauri::{Runtime, State, Emitter};
|
use tauri::{Runtime, State, Emitter};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -60,7 +61,6 @@ pub struct TranscriptionArgs {
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TranscriptionResult {
|
pub struct TranscriptionResult {
|
||||||
pub visual_description: String,
|
|
||||||
pub narrative: String,
|
pub narrative: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,8 +70,27 @@ pub struct Thumbnail {
|
||||||
pub time: u32,
|
pub time: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Removed unused Gemini request/response structs since we're handling
|
pub struct ThumbnailState {
|
||||||
// the API interaction through the Python script
|
temp_dir: Option<TempDir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThumbnailState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
temp_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThumbnailState {
|
||||||
|
pub fn set_temp_dir(&mut self, dir: TempDir) {
|
||||||
|
self.temp_dir = Some(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_temp_dir(&self) -> Option<&TempDir> {
|
||||||
|
self.temp_dir.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct ProgressUpdate {
|
pub struct ProgressUpdate {
|
||||||
|
|
@ -201,24 +220,21 @@ pub async fn test_gemini_api_impl(api_key: &str) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn extract_thumbnails_impl(video_path: &str, interval: u32) -> Result<Vec<Thumbnail>, Error> {
|
pub async fn extract_thumbnails_impl(
|
||||||
// Create a directory in the system temp directory
|
video_path: &str,
|
||||||
let temp_dir = std::env::temp_dir().join("video_thumbs");
|
interval: u32,
|
||||||
let thumb_dir = temp_dir.join(format!("session_{}", std::time::SystemTime::now()
|
thumbnail_state: State<'_, Mutex<ThumbnailState>>
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
) -> Result<Vec<Thumbnail>, Error> {
|
||||||
.unwrap()
|
// Create a new temporary directory
|
||||||
.as_secs()));
|
let temp_dir = TempDir::new().map_err(|e| Error::IoError(e))?;
|
||||||
|
println!("Extracting thumbnails to temporary directory: {:?}", temp_dir.path());
|
||||||
std::fs::create_dir_all(&thumb_dir)
|
|
||||||
.map_err(|e| Error::IoError(e))?;
|
|
||||||
|
|
||||||
println!("Extracting thumbnails to: {:?}", thumb_dir);
|
|
||||||
|
|
||||||
let duration = get_video_duration(video_path).await? as u32;
|
let duration = get_video_duration(video_path).await? as u32;
|
||||||
let mut thumbnails = Vec::new();
|
let mut thumbnails = Vec::new();
|
||||||
|
|
||||||
|
// Process all frames before moving temp_dir into state
|
||||||
for time in (0..duration).step_by(interval as usize) {
|
for time in (0..duration).step_by(interval as usize) {
|
||||||
let output_path = thumb_dir.join(format!("thumb_{}.jpg", time));
|
let output_path = temp_dir.path().join(format!("thumb_{}.jpg", time));
|
||||||
match extract_frame(video_path, time, output_path.to_str().unwrap()).await {
|
match extract_frame(video_path, time, output_path.to_str().unwrap()).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Successfully extracted frame at time {}", time);
|
println!("Successfully extracted frame at time {}", time);
|
||||||
|
|
@ -238,6 +254,8 @@ pub async fn extract_thumbnails_impl(video_path: &str, interval: u32) -> Result<
|
||||||
return Err(Error::FFmpegError("Failed to extract any thumbnails".to_string()));
|
return Err(Error::FFmpegError("Failed to extract any thumbnails".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the TempDir in the state only after we're done using it
|
||||||
|
thumbnail_state.lock().unwrap().set_temp_dir(temp_dir);
|
||||||
Ok(thumbnails)
|
Ok(thumbnails)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,7 +276,7 @@ pub async fn process_transcription_impl(args: TranscriptionArgs) -> Result<Trans
|
||||||
|
|
||||||
// Save base64 image to temp file
|
// Save base64 image to temp file
|
||||||
let frame_path = temp_dir.path().join(format!("frame_{}.jpg", frame.time));
|
let frame_path = temp_dir.path().join(format!("frame_{}.jpg", frame.time));
|
||||||
let image_data = base64::decode(&frame.base64_image)
|
let image_data = base64::engine::general_purpose::STANDARD.decode(&frame.base64_image)
|
||||||
.map_err(|e| Error::ImageError(format!("Failed to decode base64 image: {}", e)))?;
|
.map_err(|e| Error::ImageError(format!("Failed to decode base64 image: {}", e)))?;
|
||||||
fs::write(&frame_path, image_data)
|
fs::write(&frame_path, image_data)
|
||||||
.map_err(|e| Error::IoError(e))?;
|
.map_err(|e| Error::IoError(e))?;
|
||||||
|
|
@ -279,12 +297,6 @@ pub async fn process_transcription_impl(args: TranscriptionArgs) -> Result<Trans
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
// Combine all visual descriptions for the raw visual description output
|
|
||||||
let visual_description = frame_descriptions.iter()
|
|
||||||
.map(|(time, desc)| format!("At timestamp {}:\n{}", time, desc))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n\n");
|
|
||||||
|
|
||||||
// Process subtitle content if provided
|
// Process subtitle content if provided
|
||||||
let narrative = if let Some(subtitle_path) = &args.subtitle_path {
|
let narrative = if let Some(subtitle_path) = &args.subtitle_path {
|
||||||
println!("\nReading and parsing subtitle content...");
|
println!("\nReading and parsing subtitle content...");
|
||||||
|
|
@ -366,24 +378,19 @@ pub async fn process_transcription_impl(args: TranscriptionArgs) -> Result<Trans
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("\n=== Final Transcription Result ===");
|
println!("\n=== Final Transcription Result ===");
|
||||||
println!("\nVisual Description:");
|
|
||||||
println!("{}", visual_description);
|
|
||||||
println!("\nNarrative:");
|
println!("\nNarrative:");
|
||||||
println!("{}", narrative);
|
println!("{}", narrative);
|
||||||
|
|
||||||
// Create the result struct
|
// Create the result struct
|
||||||
let result = TranscriptionResult {
|
let result = TranscriptionResult {
|
||||||
visual_description: visual_description.clone(),
|
|
||||||
narrative: narrative.clone(),
|
narrative: narrative.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the final result structure
|
// Log the final result structure
|
||||||
println!("\nReturning transcription result with:");
|
println!("\nReturning transcription result with:");
|
||||||
println!("- Visual description length: {}", result.visual_description.len());
|
|
||||||
println!("- Narrative length: {}", result.narrative.len());
|
println!("- Narrative length: {}", result.narrative.len());
|
||||||
println!("\nFinal TranscriptionResult struct:");
|
println!("\nFinal TranscriptionResult struct:");
|
||||||
println!("{{");
|
println!("{{");
|
||||||
println!(" \"visualDescription\": \"{}\",", result.visual_description);
|
|
||||||
println!(" \"narrative\": \"{}\"", result.narrative);
|
println!(" \"narrative\": \"{}\"", result.narrative);
|
||||||
println!("}}");
|
println!("}}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
use subtitle_merge_lib::{
|
use subtitle_merge_lib::{
|
||||||
ProgressState, MergeArgs, Error, merge_video_subtitle_impl, check_ffmpeg,
|
ProgressState, MergeArgs, Error, merge_video_subtitle_impl, check_ffmpeg,
|
||||||
test_gemini_api_impl, extract_thumbnails_impl, process_transcription_impl,
|
test_gemini_api_impl, extract_thumbnails_impl, process_transcription_impl,
|
||||||
TranscriptionArgs, get_video_duration
|
TranscriptionArgs, get_video_duration, ThumbnailState
|
||||||
};
|
};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use base64::prelude::*;
|
use base64::Engine;
|
||||||
use tauri::Runtime;
|
use tauri::Runtime;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn check_dependencies() -> Result<(), Error> {
|
async fn check_dependencies() -> Result<(), Error> {
|
||||||
|
|
@ -40,8 +41,12 @@ async fn get_video_duration_command(path: String) -> Result<f32, Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn extract_thumbnails(path: String, interval: u32) -> Result<Vec<subtitle_merge_lib::Thumbnail>, Error> {
|
async fn extract_thumbnails(
|
||||||
extract_thumbnails_impl(&path, interval).await
|
path: String,
|
||||||
|
interval: u32,
|
||||||
|
thumbnail_state: tauri::State<'_, Mutex<ThumbnailState>>,
|
||||||
|
) -> Result<Vec<subtitle_merge_lib::Thumbnail>, Error> {
|
||||||
|
extract_thumbnails_impl(&path, interval, thumbnail_state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -50,15 +55,41 @@ async fn process_transcription(args: TranscriptionArgs) -> Result<subtitle_merge
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn generate_thumbnails(video_path: String) -> Result<Vec<subtitle_merge_lib::Thumbnail>, Error> {
|
async fn generate_thumbnails(
|
||||||
|
video_path: String,
|
||||||
|
thumbnail_state: tauri::State<'_, Mutex<ThumbnailState>>,
|
||||||
|
) -> Result<Vec<subtitle_merge_lib::Thumbnail>, Error> {
|
||||||
// Generate thumbnails every 10 seconds
|
// Generate thumbnails every 10 seconds
|
||||||
extract_thumbnails_impl(&video_path, 10).await
|
extract_thumbnails_impl(&video_path, 10, thumbnail_state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn read_thumbnail(path: String) -> Result<String, Error> {
|
async fn read_thumbnail(
|
||||||
let data = fs::read(&path).map_err(|e| Error::IoError(e))?;
|
path: String,
|
||||||
Ok(BASE64_STANDARD.encode(&data))
|
thumbnail_state: tauri::State<'_, Mutex<ThumbnailState>>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
// Verify the path is within our temp directory to prevent unauthorized access
|
||||||
|
let state = thumbnail_state.lock().unwrap();
|
||||||
|
if let Some(temp_dir) = state.get_temp_dir() {
|
||||||
|
let temp_path = temp_dir.path();
|
||||||
|
let requested_path = std::path::Path::new(&path);
|
||||||
|
|
||||||
|
// Check if the requested path is within our temp directory
|
||||||
|
if !requested_path.starts_with(temp_path) {
|
||||||
|
return Err(Error::IoError(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
"Attempted to access file outside of temporary directory",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = fs::read(&path).map_err(|e| Error::IoError(e))?;
|
||||||
|
Ok(base64::engine::general_purpose::STANDARD.encode(&data))
|
||||||
|
} else {
|
||||||
|
Err(Error::IoError(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"No active thumbnail session",
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -67,6 +98,7 @@ fn main() {
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.manage(ProgressState::default())
|
.manage(ProgressState::default())
|
||||||
|
.manage(Mutex::new(ThumbnailState::default()))
|
||||||
.setup(|_app| {
|
.setup(|_app| {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/schema.json",
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/schema.json",
|
||||||
"productName": "Video Subtitle Merger 2.0",
|
"productName": "DeafBlind Video Assistant",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"identifier": "com.subtitle-merge.app",
|
"identifier": "com.deafblind-video-assistant.app",
|
||||||
"app": {
|
"app": {
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null,
|
"csp": null,
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Video Subtitle Merger 2.0",
|
"title": "DeafBlind Video Assistant",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600,
|
"height": 600,
|
||||||
"visible": true,
|
"visible": true,
|
||||||
|
|
|
||||||
26
src/App.tsx
26
src/App.tsx
|
|
@ -41,18 +41,8 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 py-8 px-4">
|
<div className="min-h-screen bg-gray-100 py-8 px-4">
|
||||||
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold">Video Subtitle Merger <span className="text-blue-500">3.0</span></h1>
|
<h1 className="text-2xl font-bold text-center">DeafBlind Video Assistant <span className="text-blue-500">1.0</span></h1>
|
||||||
<button
|
|
||||||
onClick={() => setIsSettingsOpen(true)}
|
|
||||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 bg-gray-100 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -64,6 +54,18 @@ function App() {
|
||||||
|
|
||||||
{!selectedTask ? (
|
{!selectedTask ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 bg-gray-100 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<h2 className="text-lg font-semibold text-center mb-8">Choose a Task</h2>
|
<h2 className="text-lg font-semibold text-center mb-8">Choose a Task</h2>
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
51
src/components/ProcessingAnimation.tsx
Normal file
51
src/components/ProcessingAnimation.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import '../styles/ProcessingAnimation.css';
|
||||||
|
|
||||||
|
export const ProcessingAnimation: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||||
|
{/* Brain container with pulse and glow animation */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* Brain icon with pulse animation */}
|
||||||
|
<div className="w-16 h-16 animate-pulse-gentle text-gray-600">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="animate-spin-slow"
|
||||||
|
>
|
||||||
|
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
|
||||||
|
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Glowing effect */}
|
||||||
|
<div className="absolute inset-0 animate-glow bg-blue-400 rounded-full opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thought bubbles */}
|
||||||
|
<div className="relative h-16 w-16">
|
||||||
|
{/* Multiple bubbles with different animations */}
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="absolute left-1/2 bottom-0 w-3 h-3 rounded-full bg-white border-2 border-blue-400 thought-bubble opacity-0"
|
||||||
|
style={{
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Processing text */}
|
||||||
|
<p className="text-gray-600 font-medium animate-pulse">
|
||||||
|
Processing with AI...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
|
||||||
import { FileSelector } from './FileSelector';
|
import { FileSelector } from './FileSelector';
|
||||||
import { GeminiSettings } from './GeminiSettings';
|
import { GeminiSettings } from './GeminiSettings';
|
||||||
import { VideoTimeline } from './VideoTimeline';
|
import { VideoTimeline } from './VideoTimeline';
|
||||||
|
import { ProcessingAnimation } from './ProcessingAnimation';
|
||||||
|
|
||||||
interface FileState {
|
interface FileState {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -10,7 +11,6 @@ interface FileState {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TranscriptionState {
|
interface TranscriptionState {
|
||||||
visualDescription: string;
|
|
||||||
narrative: string;
|
narrative: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +39,6 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [videoFile, setVideoFile] = useState<FileState | null>(null);
|
const [videoFile, setVideoFile] = useState<FileState | null>(null);
|
||||||
const [subtitleFile, setSubtitleFile] = useState<FileState | null>(null);
|
const [subtitleFile, setSubtitleFile] = useState<FileState | null>(null);
|
||||||
const [progress, setProgress] = useState<number>(0);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedFrames, setSelectedFrames] = useState<SelectedFrame[]>([]);
|
const [selectedFrames, setSelectedFrames] = useState<SelectedFrame[]>([]);
|
||||||
|
|
@ -68,15 +67,12 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setProgress(0);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setTranscription(null);
|
setTranscription(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setProgress(10); // Starting
|
|
||||||
console.log('Starting transcription process...');
|
console.log('Starting transcription process...');
|
||||||
|
|
||||||
setProgress(20); // Processing frames
|
|
||||||
const transcriptionResult = await invoke<TranscriptionState>('process_transcription', {
|
const transcriptionResult = await invoke<TranscriptionState>('process_transcription', {
|
||||||
args: {
|
args: {
|
||||||
video_path: videoFile.path,
|
video_path: videoFile.path,
|
||||||
|
|
@ -92,20 +88,13 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate and clean the content
|
// Clean the content
|
||||||
const visualDescription = String(transcriptionResult.visualDescription || '').trim();
|
|
||||||
const narrative = String(transcriptionResult.narrative || '').trim();
|
const narrative = String(transcriptionResult.narrative || '').trim();
|
||||||
|
|
||||||
if (!visualDescription) {
|
|
||||||
throw new Error('Visual description is missing from the response');
|
|
||||||
}
|
|
||||||
|
|
||||||
setTranscription({
|
setTranscription({
|
||||||
visualDescription,
|
|
||||||
narrative
|
narrative
|
||||||
});
|
});
|
||||||
|
|
||||||
setProgress(100); // Complete
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Transcription error:', error);
|
console.error('Transcription error:', error);
|
||||||
setError(`Transcription error: ${error}`);
|
setError(`Transcription error: ${error}`);
|
||||||
|
|
@ -155,38 +144,19 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isProcessing && <ProcessingAnimation />}
|
||||||
|
|
||||||
{transcription && (
|
{transcription && (
|
||||||
<div className="mt-6 space-y-4">
|
<div className="mt-6 space-y-4">
|
||||||
<h3 className="text-lg font-semibold">DeafBlind Accessibility Transcription</h3>
|
<h3 className="text-lg font-semibold">Scene Narrative</h3>
|
||||||
<div className="h-[400px] overflow-y-auto p-4 bg-gray-50 rounded-lg text-sm space-y-6">
|
<div className="h-[400px] overflow-y-auto p-4 bg-gray-50 rounded-lg text-sm">
|
||||||
<div>
|
<div className="bg-white p-4 rounded border border-gray-200">
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">Visual Description</h4>
|
<p className="whitespace-pre-wrap leading-relaxed">{transcription.narrative}</p>
|
||||||
<div className="bg-white p-4 rounded border border-gray-200">
|
|
||||||
<p className="whitespace-pre-wrap leading-relaxed">{transcription.visualDescription}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-700 mb-2">Narrative</h4>
|
|
||||||
<div className="bg-white p-4 rounded border border-gray-200">
|
|
||||||
<p className="whitespace-pre-wrap leading-relaxed">{transcription.narrative}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="h-2.5 bg-gray-200 rounded-full">
|
|
||||||
<div
|
|
||||||
className="h-2.5 bg-blue-600 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-center text-sm text-gray-600">
|
|
||||||
{isProcessing ? `${Math.round(progress)}%` : ''}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
||||||
<strong className="font-bold">Error: </strong>
|
<strong className="font-bold">Error: </strong>
|
||||||
|
|
|
||||||
33
src/styles/ProcessingAnimation.css
Normal file
33
src/styles/ProcessingAnimation.css
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, 0) scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -40px) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -80px) scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thought-bubble {
|
||||||
|
animation: float ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thought-bubble:nth-child(1) {
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thought-bubble:nth-child(2) {
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thought-bubble:nth-child(3) {
|
||||||
|
animation-duration: 2.5s;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,22 @@ export default {
|
||||||
spacing: {
|
spacing: {
|
||||||
'128': '32rem',
|
'128': '32rem',
|
||||||
},
|
},
|
||||||
|
animation: {
|
||||||
|
'spin-slow': 'spin 8s linear infinite',
|
||||||
|
'pulse-gentle': 'pulse 2s ease-in-out infinite',
|
||||||
|
'glow': 'ping 2s ease-in-out infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
ping: {
|
||||||
|
'0%': { transform: 'scale(1)', opacity: '0.2' },
|
||||||
|
'50%': { transform: 'scale(1.2)', opacity: '0.3' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '0.2' },
|
||||||
|
},
|
||||||
|
pulse: {
|
||||||
|
'0%, 100%': { transform: 'scale(1)' },
|
||||||
|
'50%': { transform: 'scale(1.05)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue