Empty default API key
Warning message in TranscriptionTask when API key is empty "Add API Key in Settings" button that opens settings modal Error message when trying to test empty API key No "Reset to Default" button for API key Improved copy functionality with inline success message Added fade-in animation for the success message
This commit is contained in:
parent
54db9d1904
commit
436c8132d3
12 changed files with 127 additions and 80 deletions
BIN
gemini/gemini-query-aarch64-apple-darwin
Executable file
BIN
gemini/gemini-query-aarch64-apple-darwin
Executable file
Binary file not shown.
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
|
@ -3888,6 +3888,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.7",
|
||||
"futures-util",
|
||||
"image",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ tauri = { version = "2.0.0", features = [] }
|
|||
tokio = { version = "1.36", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
futures-util = "0.3"
|
||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
|
|
|
|||
BIN
src-tauri/binaries/gemini-query-aarch64-apple-darwin
Executable file
BIN
src-tauri/binaries/gemini-query-aarch64-apple-darwin
Executable file
Binary file not shown.
|
|
@ -24,7 +24,13 @@
|
|||
},
|
||||
{
|
||||
"identifier": "shell:allow-execute",
|
||||
"description": "Allows executing FFmpeg commands"
|
||||
"description": "Allows executing FFmpeg commands and sidecars",
|
||||
"allow": [
|
||||
{
|
||||
"name": "gemini-query",
|
||||
"sidecar": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "core:event:allow-emit",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize};
|
|||
use std::process::{Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::io::BufRead;
|
||||
use tauri::{Runtime, State, Emitter, Manager};
|
||||
use tauri::{Runtime, State, Emitter};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tempfile::TempDir;
|
||||
use std::fs;
|
||||
use base64::Engine;
|
||||
|
|
@ -174,30 +175,17 @@ async fn query_gemini<R: Runtime>(
|
|||
println!("Image Path: {}", path);
|
||||
}
|
||||
|
||||
let executable_path = if cfg!(debug_assertions) {
|
||||
// Development mode - use absolute path
|
||||
std::path::PathBuf::from("/Users/chaulmark/homemade-scripts/video-subtitle-merger/subtitle-merge/gemini/gemini-query")
|
||||
} else {
|
||||
// Production mode - use resource path
|
||||
app_handle.path().resource_dir()
|
||||
.map_err(|e| Error::IoError(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Resource directory not found: {}", e)
|
||||
)))?
|
||||
.join("gemini-query")
|
||||
};
|
||||
|
||||
println!("Executable path: {:?}", executable_path);
|
||||
let mut command = Command::new(executable_path);
|
||||
command.arg(api_key)
|
||||
.arg(prompt);
|
||||
let sidecar = app_handle.shell().sidecar("gemini-query")
|
||||
.map_err(|e| Error::GeminiError(format!("Failed to get sidecar: {}", e)))?;
|
||||
|
||||
let mut command = sidecar.arg(api_key).arg(prompt);
|
||||
|
||||
if let Some(path) = image_path {
|
||||
command.arg(path);
|
||||
command = command.arg(path);
|
||||
}
|
||||
|
||||
println!("\nExecuting command: {:?}", command);
|
||||
let output = command.output()
|
||||
println!("\nExecuting sidecar command");
|
||||
let output = command.output().await
|
||||
.map_err(|e| Error::GeminiError(format!("Failed to execute Gemini query: {}", e)))?;
|
||||
|
||||
let response_text = String::from_utf8_lossy(&output.stdout);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,13 @@
|
|||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["app"],
|
||||
"targets": ["dmg", "app"],
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "10.13",
|
||||
"signingIdentity": "Developer ID Application: Chris Haulmark (RJHWWWSF6Q)",
|
||||
"providerShortName": null
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
|
@ -36,8 +42,8 @@
|
|||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"../gemini/gemini-query"
|
||||
"externalBin": [
|
||||
"binaries/gemini-query"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function App() {
|
|||
const [selectedTask, setSelectedTask] = useState<TaskType>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [apiKey, setApiKey] = useState('AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [visualPrompt, setVisualPrompt] = useState(() => {
|
||||
const saved = localStorage.getItem('visualPrompt');
|
||||
return saved || DEFAULT_VISUAL_PROMPT;
|
||||
|
|
@ -110,6 +110,7 @@ function App() {
|
|||
apiKey={apiKey}
|
||||
visualPrompt={visualPrompt}
|
||||
subtitlePrompt={subtitlePrompt}
|
||||
onOpenSettings={() => setIsSettingsOpen(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ interface SettingsProps {
|
|||
|
||||
// Note: We keep subtitle prompt in Settings since it's still used by the merge task
|
||||
|
||||
export const DEFAULT_API_KEY = 'AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc';
|
||||
export const DEFAULT_API_KEY = '';
|
||||
export const DEFAULT_VISUAL_PROMPT = `Analyze this video frame from timestamp {} and provide a visual description focusing on:
|
||||
1. The visual setup and environment
|
||||
2. The people present, their appearance, and positioning
|
||||
|
|
@ -83,10 +83,6 @@ export const Settings: React.FC<SettingsProps> = ({
|
|||
setDisplayValue('*'.repeat(apiKey.length));
|
||||
};
|
||||
|
||||
const resetApiKeyToDefault = () => {
|
||||
onApiKeyChange(DEFAULT_API_KEY);
|
||||
};
|
||||
|
||||
const resetVisualPromptToDefault = () => {
|
||||
onVisualPromptChange(DEFAULT_VISUAL_PROMPT);
|
||||
};
|
||||
|
|
@ -98,6 +94,13 @@ export const Settings: React.FC<SettingsProps> = ({
|
|||
const testApiKey = async () => {
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
|
||||
if (!apiKey.trim()) {
|
||||
setTestResult('Error: API Key is required for testing');
|
||||
setTesting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('test_gemini_api', { apiKey });
|
||||
setTestResult('API key is valid');
|
||||
|
|
@ -145,6 +148,14 @@ export const Settings: React.FC<SettingsProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<a
|
||||
href="https://aistudio.google.com/app/apikey"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Get API Key
|
||||
</a>
|
||||
<button
|
||||
onClick={testApiKey}
|
||||
disabled={testing}
|
||||
|
|
@ -152,15 +163,6 @@ export const Settings: React.FC<SettingsProps> = ({
|
|||
>
|
||||
{testing ? 'Testing...' : 'Test API Key'}
|
||||
</button>
|
||||
|
||||
{apiKey !== DEFAULT_API_KEY && (
|
||||
<button
|
||||
onClick={resetApiKeyToDefault}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Reset to Default
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
|
|
@ -174,19 +176,14 @@ export const Settings: React.FC<SettingsProps> = ({
|
|||
<label className="block text-sm font-medium text-gray-700">
|
||||
Visual Description Prompt
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={visualPrompt}
|
||||
onChange={(e) => onVisualPromptChange(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm font-mono resize-y"
|
||||
placeholder="Enter prompt for visual description"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 text-xs text-gray-500">
|
||||
{visualPrompt.length} characters
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
value={visualPrompt}
|
||||
onChange={(e) => onVisualPromptChange(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm font-mono resize-y"
|
||||
placeholder="Enter prompt for visual description"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{visualPrompt !== DEFAULT_VISUAL_PROMPT && (
|
||||
<button
|
||||
onClick={resetVisualPromptToDefault}
|
||||
|
|
@ -201,19 +198,14 @@ export const Settings: React.FC<SettingsProps> = ({
|
|||
<label className="block text-sm font-medium text-gray-700">
|
||||
Subtitle Narrative Prompt
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={subtitlePrompt}
|
||||
onChange={(e) => onSubtitlePromptChange(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm font-mono resize-y"
|
||||
placeholder="Enter prompt for subtitle narrative"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 text-xs text-gray-500">
|
||||
{subtitlePrompt.length} characters
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
value={subtitlePrompt}
|
||||
onChange={(e) => onSubtitlePromptChange(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm font-mono resize-y"
|
||||
placeholder="Enter prompt for subtitle narrative"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{subtitlePrompt !== DEFAULT_SUBTITLE_PROMPT && (
|
||||
<button
|
||||
onClick={resetSubtitlePromptToDefault}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ interface TranscriptionTaskProps {
|
|||
apiKey: string;
|
||||
visualPrompt: string;
|
||||
subtitlePrompt: string;
|
||||
onOpenSettings?: () => void;
|
||||
}
|
||||
|
||||
export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
||||
apiKey,
|
||||
visualPrompt,
|
||||
subtitlePrompt,
|
||||
onOpenSettings
|
||||
}) => {
|
||||
const [videoFile, setVideoFile] = useState<FileState | null>(null);
|
||||
const [subtitleFile, setSubtitleFile] = useState<FileState | null>(null);
|
||||
|
|
@ -44,6 +46,7 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
|||
const [selectedFrames, setSelectedFrames] = useState<SelectedFrame[]>([]);
|
||||
const [transcription, setTranscription] = useState<TranscriptionState | null>(null);
|
||||
const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
const handleVideoSelect = (path: string) => {
|
||||
const name = path.split('/').pop() || '';
|
||||
|
|
@ -105,6 +108,28 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
|||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{!apiKey && (
|
||||
<div className="bg-amber-50 border-l-4 border-amber-400 p-4 mb-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-amber-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-amber-700">
|
||||
A Gemini API Key is required to generate video descriptions.
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
className="ml-2 font-medium text-amber-700 underline hover:text-amber-600"
|
||||
>
|
||||
Add API Key in Settings
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 mr-4">
|
||||
|
|
@ -149,9 +174,32 @@ export const TranscriptionTask: React.FC<TranscriptionTaskProps> = ({
|
|||
{transcription && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<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">
|
||||
<div className="bg-white p-4 rounded border border-gray-200">
|
||||
<p className="whitespace-pre-wrap leading-relaxed">{transcription.narrative}</p>
|
||||
<div className="space-y-2">
|
||||
<div className="h-[400px] overflow-y-auto p-4 bg-gray-50 rounded-lg text-sm">
|
||||
<div className="bg-white p-4 rounded border border-gray-200">
|
||||
<p className="whitespace-pre-wrap leading-relaxed">{transcription.narrative}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent any navigation
|
||||
navigator.clipboard.writeText(transcription.narrative)
|
||||
.then(() => {
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
})
|
||||
.catch(err => console.error('Failed to copy:', err));
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
{copySuccess && (
|
||||
<span className="text-sm text-green-600 animate-fade-in">
|
||||
Content copied successfully!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,19 +21,8 @@ interface SelectedFrame {
|
|||
|
||||
export const VideoTimeline: React.FC<VideoTimelineProps> = ({ videoPath, onFramesSelect, thumbnails }) => {
|
||||
const [selectedFrames, setSelectedFrames] = useState<SelectedFrame[]>([]);
|
||||
const [duration, setDuration] = useState<number>(0);
|
||||
const [thumbnailUrls, setThumbnailUrls] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (videoPath) {
|
||||
invoke<number>('get_video_duration', { path: videoPath })
|
||||
.then(videoDuration => setDuration(videoDuration))
|
||||
.catch(error => console.error('Error getting video duration:', error));
|
||||
} else {
|
||||
setDuration(0);
|
||||
}
|
||||
}, [videoPath]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadThumbnails = async () => {
|
||||
const urls: { [key: string]: string } = {};
|
||||
|
|
|
|||
|
|
@ -21,3 +21,18 @@
|
|||
|
||||
/* Remove unused progress bar styles since we're using direct classes in App.tsx */
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.2s ease-in;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue