Phase 6: Llama-server manager, settings UI, packaging, and polish
- Implement LlamaManager in Rust for llama-server lifecycle: spawn with port allocation, health check, clean shutdown on Drop, model listing - Add llama_start/stop/status/list_models Tauri commands - Add load_settings/save_settings commands with JSON persistence - Build SettingsModal with tabs for Transcription, AI Provider, Local AI settings (model size, device, language, API keys, provider selection) - Wire settings into pipeline calls (model, device, language, skip diarization) - Configure Tauri packaging: asset protocol for local audio files, CSP policy, bundle metadata, Linux .deb/.AppImage and Windows .msi config - Add keyboard shortcuts: Space (play/pause), Ctrl+O (import), Ctrl+, (settings), Escape (close menus/modals) - Close export dropdown on outside click - Tests: 30 Python, 6 Rust, 0 Svelte errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,58 @@
|
||||
import SpeakerManager from '$lib/components/SpeakerManager.svelte';
|
||||
import AIChatPanel from '$lib/components/AIChatPanel.svelte';
|
||||
import ProgressOverlay from '$lib/components/ProgressOverlay.svelte';
|
||||
import SettingsModal from '$lib/components/SettingsModal.svelte';
|
||||
import { segments, speakers } from '$lib/stores/transcript';
|
||||
import { settings, loadSettings } from '$lib/stores/settings';
|
||||
import type { Segment, Speaker } from '$lib/types/transcript';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let waveformPlayer: WaveformPlayer;
|
||||
let audioUrl = $state('');
|
||||
let showSettings = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
loadSettings();
|
||||
|
||||
// Global keyboard shortcuts
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Don't trigger shortcuts when typing in inputs
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
||||
|
||||
if (e.key === ' ' && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
waveformPlayer?.togglePlayPause?.();
|
||||
} else if (e.key === 'o' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
handleFileImport();
|
||||
} else if (e.key === ',' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
showSettings = true;
|
||||
} else if (e.key === 'Escape') {
|
||||
showExportMenu = false;
|
||||
showSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close export dropdown on outside click
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (showExportMenu) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.export-dropdown')) {
|
||||
showExportMenu = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
});
|
||||
let isTranscribing = $state(false);
|
||||
let transcriptionProgress = $state(0);
|
||||
let transcriptionStage = $state('');
|
||||
@@ -61,7 +108,13 @@
|
||||
duration_ms: number;
|
||||
speakers: string[];
|
||||
num_speakers: number;
|
||||
}>('run_pipeline', { filePath });
|
||||
}>('run_pipeline', {
|
||||
filePath,
|
||||
model: $settings.transcription_model || undefined,
|
||||
device: $settings.transcription_device || undefined,
|
||||
language: $settings.transcription_language || undefined,
|
||||
skipDiarization: $settings.skip_diarization || undefined,
|
||||
});
|
||||
|
||||
// Create speaker entries from pipeline result
|
||||
const newSpeakers: Speaker[] = (result.speakers || []).map((label, idx) => ({
|
||||
@@ -167,6 +220,9 @@
|
||||
<button class="import-btn" onclick={handleFileImport}>
|
||||
Import Audio/Video
|
||||
</button>
|
||||
<button class="settings-btn" onclick={() => showSettings = true} title="Settings">
|
||||
Settings
|
||||
</button>
|
||||
{#if $segments.length > 0}
|
||||
<div class="export-dropdown">
|
||||
<button class="export-btn" onclick={() => showExportMenu = !showExportMenu}>
|
||||
@@ -204,6 +260,11 @@
|
||||
message={transcriptionMessage}
|
||||
/>
|
||||
|
||||
<SettingsModal
|
||||
visible={showSettings}
|
||||
onClose={() => showSettings = false}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.app-header {
|
||||
display: flex;
|
||||
@@ -235,6 +296,19 @@
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.settings-btn {
|
||||
background: none;
|
||||
border: 1px solid #4a5568;
|
||||
color: #e0e0e0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.settings-btn:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-color: #e94560;
|
||||
}
|
||||
.export-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user