diff --git a/apps/whispering/src/lib/components/settings/LocalModelSelector.svelte b/apps/whispering/src/lib/components/settings/LocalModelSelector.svelte index 1e9b5aec9c..d50e2d351a 100644 --- a/apps/whispering/src/lib/components/settings/LocalModelSelector.svelte +++ b/apps/whispering/src/lib/components/settings/LocalModelSelector.svelte @@ -266,7 +266,7 @@ {:else if !value && customEntries.length === 0} - + diff --git a/apps/whispering/src/routes/(app)/+page.svelte b/apps/whispering/src/routes/(app)/+page.svelte index 45d18a73cb..50b0161f28 100644 --- a/apps/whispering/src/routes/(app)/+page.svelte +++ b/apps/whispering/src/routes/(app)/+page.svelte @@ -2,10 +2,14 @@ import { Button } from '@epicenter/ui/button'; import { confirmationDialog } from '@epicenter/ui/confirmation-dialog'; import { FileDropZone } from '@epicenter/ui/file-drop-zone'; + import * as Item from '@epicenter/ui/item'; import * as Kbd from '@epicenter/ui/kbd'; import { Link } from '@epicenter/ui/link'; import * as SectionHeader from '@epicenter/ui/section-header'; import * as ToggleGroup from '@epicenter/ui/toggle-group'; + import Cpu from '@lucide/svelte/icons/cpu'; + import Heart from '@lucide/svelte/icons/heart'; + import ShieldCheck from '@lucide/svelte/icons/shield-check'; import XIcon from '@lucide/svelte/icons/x'; import { createQuery } from '@tanstack/svelte-query'; import type { UnlistenFn } from '@tauri-apps/api/event'; @@ -179,10 +183,15 @@ Whispering -
- +{#snippet hero(layout: 'center' | 'split')} +
- + Press shortcut → speak → get text. Free and open source ❤️
+{/snippet} - - +
{#if !transcriptionReadiness.isReady} -
-
-

Set up transcription

-

- {transcriptionReadiness.primaryIssue ?? - 'Choose how Whispering turns your speech into text.'} -

-
- -
- {:else} - captureSurface.current, - (surface) => { - if (!surface) return; - void selectCaptureSurface(surface as CaptureSurface); - }} - class="w-full" - > - {#each CAPTURE_SURFACE_OPTIONS as option} - {@const SurfaceIcon = CAPTURE_SURFACE_META[option.value].Icon} - + +
+
- - - - {/each} - + {@render hero('split')} + + + + + + + Private and offline + + Audio is transcribed on your device and never uploaded. + + + + + + + + + Runs on this device + + No servers, no API keys, no monthly bill. + + + + + + + + + Free and open source + + Yours to keep, audit, and extend. + + + + +
- - {#if captureSurface.current === 'manual'} -
- - {#snippet pipeline()} - - - - - - - {/snippet} - - {#if manualRecorder.state === 'RECORDING'} - - {/if} -
- {:else if captureSurface.current === 'vad'} -
- - {#snippet pipeline()} - - - - - - - {/snippet} - -
- {:else if captureSurface.current === 'import'} -
- { - if (files.length > 0) { - await importFiles({ files }); - } - }} - onFileRejected={({ file, reason }) => { - report.error({ - cause: PageError.FileRejected({ - fileName: file.name, - reason, - }).error, - title: 'File rejected', - }); - }} - class="h-32 sm:h-36 w-full" - /> - - - +
+

Set up transcription

+

+ {transcriptionReadiness.primaryIssue ?? + 'Choose how Whispering turns your speech into text.'} +

+
+ -
+
- {/if} +
+ {:else} +
+ {@render hero('center')} - {#if latestRecording} -
- { - confirmationDialog.open({ - title: 'Delete recording', - description: 'Are you sure you want to delete this recording?', - confirm: { text: 'Delete', variant: 'destructive' }, - onConfirm: () => { - services.blobs.audio.revokeUrl(latestRecording.id); - recordings.delete(latestRecording.id); - report.success({ - title: 'Deleted recording!', - description: 'Your recording has been deleted.', - }); - }, - }); + + captureSurface.current, + (surface) => { + if (!surface) return; + void selectCaptureSurface(surface as CaptureSurface); }} - /> - - {#if audioPlaybackUrlQuery.data} - - {/if} -
- {/if} + class="w-full" + > + {#each CAPTURE_SURFACE_OPTIONS as option} + {@const SurfaceIcon = CAPTURE_SURFACE_META[option.value].Icon} + + + + + {/each} + -
+ {#if captureSurface.current === 'manual'} -

- {#if manualShortcutLabel} - Click the microphone to record{tauri ? ' here' : ''}, or press - + + {#snippet pipeline()} + + + + + + + {/snippet} + + {#if manualRecorder.state === 'RECORDING'} + {/if} -

+
{:else if captureSurface.current === 'vad'} -

- {#if vadShortcutLabel} - Click the microphone to listen{tauri ? ' here' : ''}, or press +

+ + {#snippet pipeline()} + + + + + + + {/snippet} + +
+ {:else if captureSurface.current === 'import'} +
+ { + if (files.length > 0) { + await importFiles({ files }); + } + }} + onFileRejected={({ file, reason }) => { + report.error({ + cause: PageError.FileRejected({ + fileName: file.name, + reason, + }).error, + title: 'File rejected', + }); + }} + class="h-32 sm:h-36 w-full" + /> + + + + +
+ {/if} + + {#if latestRecording} +
+ { + confirmationDialog.open({ + title: 'Delete recording', + description: 'Are you sure you want to delete this recording?', + confirm: { text: 'Delete', variant: 'destructive' }, + onConfirm: () => { + services.blobs.audio.revokeUrl(latestRecording.id); + recordings.delete(latestRecording.id); + report.success({ + title: 'Deleted recording!', + description: 'Your recording has been deleted.', + }); + }, + }); + }} + /> + + {#if audioPlaybackUrlQuery.data} + + {/if} +
+ {/if} + +
+ {#if captureSurface.current === 'manual'} +

+ {#if manualShortcutLabel} + Click the microphone to record{tauri ? ' here' : ''}, or press + + {manualShortcutLabel} + + {tauri ? 'to record from anywhere.' : 'to record.'} + {:else if tauri} + Click the microphone to record, or + set a global shortcut + to record from anywhere. + {:else} + Click the microphone to start recording. + {/if} +

+ {:else if captureSurface.current === 'vad'} +

+ {#if vadShortcutLabel} + Click the microphone to listen{tauri ? ' here' : ''}, or press + + {vadShortcutLabel} + + {tauri ? 'to listen from anywhere.' : 'to listen.'} + {:else if tauri} + Click the microphone to start a voice activated session, or + set a global shortcut + to listen from anywhere. + {:else} + Click the microphone to start a voice activated session. + {/if} +

+ {/if} +

+ {#if !tauri} + Tired of switching tabs? - {vadShortcutLabel} + Get the native desktop app - {tauri ? 'to listen from anywhere.' : 'to listen.'} - {:else if tauri} - Click the microphone to start a voice activated session, or - set a global shortcut - to listen from anywhere. - {:else} - Click the microphone to start a voice activated session. {/if}

- {/if} -

- {#if !tauri} - Tired of switching tabs? - - Get the native desktop app - - {/if} -

+
{/if}
diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000000..e22998f0d7 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,7 @@ +[install] +# Pin the dependency linker so every machine and CI produce the same +# node_modules layout, instead of leaving it to bun's default (which varies by +# bun version, workspace setup, and lockfile configVersion). "isolated" is the +# strict pnpm-style layout: a package can only import what it declares, so +# phantom dependencies fail loudly instead of riding on accidental hoisting. +linker = "isolated"