feat(capture): dictation, personalities, 0.5.0

Ships the Capture release end to end. Global-hotkey dictation with
synthetic paste into the focused app on macOS and Windows, an on-screen
pill across recording / transcribing / refining, customizable push-to-
talk and toggle chords, and an accessibility-permission prompt scoped to
Settings → Captures with inline re-check feedback.

Voice profiles gain optional personalities that power compose / rewrite /
respond actions via a local Qwen3 LLM — shared with refinement, so there
is one local LLM in the app, not two.

Refinement hardened with deterministic Whisper-loop collapse before the
LLM sees the transcript, per-capture flag snapshots for re-runs, and a
ten-transcript evaluation harness across every bundled refinement size.

Version bump 0.4.5 → 0.5.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
James Pine
2026-04-22 18:49:16 -07:00
co-authored by Claude Opus 4.7
parent ed2eec591a
commit 87c582ad54
84 changed files with 11043 additions and 512 deletions
@@ -77,6 +77,7 @@ function makeProfileSchema(t: (key: string) => string) {
name: z.string().min(1, t('profileForm.validation.nameRequired')).max(100),
description: z.string().max(500).optional(),
language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
personality: z.string().max(2000).optional(),
sampleFile: z.instanceof(File).optional(),
referenceText: z.string().max(1000).optional(),
avatarFile: z.instanceof(File).optional(),
@@ -100,6 +101,7 @@ type ProfileFormValues = {
name: string;
description?: string;
language: LanguageCode;
personality?: string;
sampleFile?: File;
referenceText?: string;
avatarFile?: File;
@@ -166,6 +168,7 @@ export function ProfileForm() {
name: '',
description: '',
language: 'en',
personality: '',
sampleFile: undefined,
referenceText: '',
avatarFile: undefined,
@@ -331,6 +334,7 @@ export function ProfileForm() {
name: editingProfile.name,
description: editingProfile.description || '',
language: editingProfile.language as LanguageCode,
personality: editingProfile.personality || '',
sampleFile: undefined,
referenceText: undefined,
avatarFile: undefined,
@@ -344,6 +348,7 @@ export function ProfileForm() {
name: profileFormDraft.name,
description: profileFormDraft.description,
language: profileFormDraft.language as LanguageCode,
personality: profileFormDraft.personality || '',
referenceText: profileFormDraft.referenceText,
sampleFile: undefined,
avatarFile: undefined,
@@ -368,6 +373,7 @@ export function ProfileForm() {
name: '',
description: '',
language: 'en',
personality: '',
sampleFile: undefined,
referenceText: undefined,
avatarFile: undefined,
@@ -493,6 +499,7 @@ export function ProfileForm() {
description: data.description,
language: data.language,
default_engine: defaultEngine || undefined,
personality: data.personality?.trim() ? data.personality.trim() : undefined,
},
});
@@ -558,6 +565,7 @@ export function ProfileForm() {
preset_engine: selectedPresetEngine,
preset_voice_id: selectedPresetVoiceId,
default_engine: selectedPresetEngine,
personality: data.personality?.trim() ? data.personality.trim() : undefined,
});
// Handle avatar upload if provided
@@ -654,6 +662,7 @@ export function ProfileForm() {
description: data.description,
language: data.language,
default_engine: defaultEngine || undefined,
personality: data.personality?.trim() ? data.personality.trim() : undefined,
});
// Convert non-WAV uploads to WAV so the backend can always use soundfile.
@@ -756,6 +765,7 @@ export function ProfileForm() {
name: values.name || '',
description: values.description || '',
language: values.language || 'en',
personality: values.personality || '',
referenceText: values.referenceText || '',
sampleMode,
};
@@ -1182,6 +1192,27 @@ export function ProfileForm() {
)}
/>
<FormField
control={form.control}
name="personality"
render={({ field }) => (
<FormItem>
<FormLabel>Personality</FormLabel>
<FormControl>
<Textarea
placeholder="Optional. Who this voice is and how they talk. E.g. &quot;a grumpy pirate who only speaks in nautical metaphors&quot;. Used by Compose, Rewrite, and the Speak API."
className="min-h-[96px]"
{...field}
/>
</FormControl>
<FormDescription>
Leave blank to hide the Compose and Rewrite buttons on the generate page.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"