Cross-platform distribution, UI improvements, and performance optimizations

- PyInstaller frozen sidecar: spec file, build script, and ffmpeg path resolver
  for self-contained distribution without Python prerequisites
- Dual-mode sidecar launcher: frozen binary (production) with dev mode fallback
- Parallel transcription + diarization pipeline (~30-40% faster)
- GPU auto-detection for diarization (CUDA when available)
- Async run_pipeline command for real-time progress event delivery
- Web Audio API backend for instant playback and seeking
- OpenAI-compatible provider replacing LiteLLM client-side routing
- Cross-platform RAM detection (Linux/macOS/Windows)
- Settings: speaker count hint, token reveal toggles, dark dropdown styling
- Loading splash screen, flexbox layout fix for viewport overflow
- Gitea Actions CI/CD pipeline (Linux, Windows, macOS ARM)
- Updated README and CLAUDE.md documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-20 21:33:43 -07:00
parent 42ccd3e21d
commit 58faa83cb3
27 changed files with 1301 additions and 283 deletions

View File

@@ -13,6 +13,7 @@
import type { Segment, Speaker } from '$lib/types/transcript';
import { onMount, tick } from 'svelte';
let appReady = $state(false);
let waveformPlayer: WaveformPlayer;
let audioUrl = $state('');
let showSettings = $state(false);
@@ -54,6 +55,8 @@
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('click', handleClickOutside);
appReady = true;
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('click', handleClickOutside);
@@ -200,6 +203,7 @@
language: $settings.transcription_language || undefined,
skipDiarization: $settings.skip_diarization || undefined,
hfToken: $settings.hf_token || undefined,
numSpeakers: $settings.num_speakers && $settings.num_speakers > 0 ? $settings.num_speakers : undefined,
});
// Create speaker entries from pipeline result
@@ -303,60 +307,70 @@
}
</script>
<div class="app-header">
<h1>Voice to Notes</h1>
<div class="header-actions">
<button class="import-btn" onclick={handleFileImport} disabled={isTranscribing}>
{#if isTranscribing}
Processing...
{:else}
Import Audio/Video
{/if}
</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}>
Export
</button>
{#if showExportMenu}
<div class="export-menu">
{#each exportFormats as fmt}
<button class="export-option" onclick={() => handleExport(fmt.format, fmt.ext, fmt.name)}>
{fmt.name} (.{fmt.ext})
</button>
{/each}
</div>
{#if !appReady}
<div class="splash-screen">
<h1 class="splash-title">Voice to Notes</h1>
<p class="splash-subtitle">Loading...</p>
<div class="splash-spinner"></div>
</div>
{:else}
<div class="app-shell">
<div class="app-header">
<h1>Voice to Notes</h1>
<div class="header-actions">
<button class="import-btn" onclick={handleFileImport} disabled={isTranscribing}>
{#if isTranscribing}
Processing...
{:else}
Import Audio/Video
{/if}
</div>
{/if}
</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}>
Export
</button>
{#if showExportMenu}
<div class="export-menu">
{#each exportFormats as fmt}
<button class="export-option" onclick={() => handleExport(fmt.format, fmt.ext, fmt.name)}>
{fmt.name} (.{fmt.ext})
</button>
{/each}
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
<div class="workspace">
<div class="main-content">
<WaveformPlayer bind:this={waveformPlayer} {audioUrl} />
<TranscriptEditor onWordClick={handleWordClick} />
<div class="workspace">
<div class="main-content">
<WaveformPlayer bind:this={waveformPlayer} {audioUrl} />
<TranscriptEditor onWordClick={handleWordClick} />
</div>
<div class="sidebar-right">
<SpeakerManager />
<AIChatPanel />
</div>
</div>
<div class="sidebar-right">
<SpeakerManager />
<AIChatPanel />
</div>
</div>
<ProgressOverlay
visible={isTranscribing}
percent={transcriptionProgress}
stage={transcriptionStage}
message={transcriptionMessage}
/>
<ProgressOverlay
visible={isTranscribing}
percent={transcriptionProgress}
stage={transcriptionStage}
message={transcriptionMessage}
/>
<SettingsModal
visible={showSettings}
onClose={() => showSettings = false}
/>
<SettingsModal
visible={showSettings}
onClose={() => showSettings = false}
/>
{/if}
<style>
.app-header {
@@ -453,11 +467,18 @@
.export-option:hover {
background: rgba(233, 69, 96, 0.2);
}
.app-shell {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.workspace {
display: flex;
gap: 1rem;
padding: 1rem;
height: calc(100vh - 3rem);
flex: 1;
min-height: 0;
overflow: hidden;
background: #0a0a23;
}
@@ -467,6 +488,8 @@
flex-direction: column;
gap: 1rem;
min-width: 0;
min-height: 0;
overflow-y: auto;
}
.sidebar-right {
width: 300px;
@@ -474,5 +497,38 @@
flex-direction: column;
gap: 1rem;
flex-shrink: 0;
min-height: 0;
overflow-y: auto;
}
.splash-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: #0a0a23;
color: #e0e0e0;
gap: 1rem;
}
.splash-title {
font-size: 2rem;
margin: 0;
color: #e94560;
}
.splash-subtitle {
font-size: 1rem;
color: #888;
margin: 0;
}
.splash-spinner {
width: 32px;
height: 32px;
border: 3px solid #2a3a5e;
border-top-color: #e94560;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>