feat(stt): add server-side STT provider via OpenAI-compatible Whisper endpoint (#860)
Co-authored-by: Alexander Busse <alex@ableph.net>
This commit is contained in:
committed by
GitHub
co-authored by
Alexander Busse
parent
75a10ea66c
commit
3746d99a85
@@ -16,6 +16,7 @@ import { NumberInput } from '@/components/ui/number-input';
|
||||
import { RiPlayLine, RiStopLine, RiCloseLine, RiAppleLine, RiInformationLine } from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { browserVoiceService } from '@/lib/voice/browserVoiceService';
|
||||
import { audioStreamService } from '@/lib/voice/audioStreamService';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
@@ -71,6 +72,19 @@ export const VoiceSettings: React.FC = () => {
|
||||
const openaiApiKey = useConfigStore((state) => state.openaiApiKey);
|
||||
const setOpenaiApiKey = useConfigStore((state) => state.setOpenaiApiKey);
|
||||
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
|
||||
// STT settings
|
||||
const sttProvider = useConfigStore((state) => state.sttProvider);
|
||||
const setSttProvider = useConfigStore((state) => state.setSttProvider);
|
||||
const sttServerUrl = useConfigStore((state) => state.sttServerUrl);
|
||||
const setSttServerUrl = useConfigStore((state) => state.setSttServerUrl);
|
||||
const sttModel = useConfigStore((state) => state.sttModel);
|
||||
const setSttModel = useConfigStore((state) => state.setSttModel);
|
||||
const sttLanguage = useConfigStore((state) => state.sttLanguage);
|
||||
const setSttLanguage = useConfigStore((state) => state.setSttLanguage);
|
||||
const sttSilenceThresholdDb = useConfigStore((state) => state.sttSilenceThresholdDb);
|
||||
const setSttSilenceThresholdDb = useConfigStore((state) => state.setSttSilenceThresholdDb);
|
||||
const sttSilenceHoldMs = useConfigStore((state) => state.sttSilenceHoldMs);
|
||||
const setSttSilenceHoldMs = useConfigStore((state) => state.setSttSilenceHoldMs);
|
||||
const setShowMessageTTSButtons = useConfigStore((state) => state.setShowMessageTTSButtons);
|
||||
const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled);
|
||||
const setVoiceModeEnabled = useConfigStore((state) => state.setVoiceModeEnabled);
|
||||
@@ -563,6 +577,146 @@ export const VoiceSettings: React.FC = () => {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Speech Recognition */}
|
||||
{voiceModeEnabled && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
Speech Recognition
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0">
|
||||
<div className="pb-1.5 pt-0.5">
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="typography-ui-label text-foreground">Provider</span>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
<ul className="space-y-1">
|
||||
<li><strong>Browser:</strong> Web Speech API (Chrome/Edge). Free, no setup.</li>
|
||||
<li><strong>Server:</strong> OpenAI-compatible Whisper server. Better accuracy, any language.</li>
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => setSttProvider('browser')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
sttProvider === 'browser'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
>
|
||||
Browser
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => setSttProvider('server')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
sttProvider === 'server'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
>
|
||||
Server
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sttProvider === 'server' && (
|
||||
<div className="py-1.5 space-y-2">
|
||||
{!audioStreamService.isSupported() && (
|
||||
<p className="typography-meta text-[var(--status-error)]">
|
||||
MediaRecorder or AudioContext is not available in this browser. Server STT may not work.
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
<span className={cn("typography-ui-label text-foreground", !sttServerUrl.trim() && "text-[var(--status-error)]")}>
|
||||
Server URL
|
||||
</span>
|
||||
<span className="typography-meta ml-2 text-muted-foreground">
|
||||
Base URL of the Whisper-compatible server
|
||||
</span>
|
||||
<div className="relative mt-1.5 max-w-xs">
|
||||
<input
|
||||
type="text"
|
||||
value={sttServerUrl}
|
||||
onChange={(e) => setSttServerUrl(e.target.value)}
|
||||
placeholder="http://localhost:8001/v1"
|
||||
className="w-full h-7 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary/70"
|
||||
/>
|
||||
{sttServerUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSttServerUrl('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<RiCloseLine className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="typography-ui-label text-foreground">Model</span>
|
||||
<div className="relative mt-1.5 max-w-xs">
|
||||
<input
|
||||
type="text"
|
||||
value={sttModel}
|
||||
onChange={(e) => setSttModel(e.target.value)}
|
||||
placeholder="deepdml/faster-whisper-large-v3-turbo-ct2"
|
||||
className="w-full h-7 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary/70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="typography-ui-label text-foreground">Language</span>
|
||||
<span className="typography-meta ml-2 text-muted-foreground">
|
||||
BCP-47 code (e.g. en, fr). Leave blank for auto-detect.
|
||||
</span>
|
||||
<div className="relative mt-1.5 max-w-[8rem]">
|
||||
<input
|
||||
type="text"
|
||||
value={sttLanguage}
|
||||
onChange={(e) => setSttLanguage(e.target.value)}
|
||||
placeholder="auto"
|
||||
className="w-full h-7 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary/70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-8 py-0.5">
|
||||
<span className="typography-ui-label text-foreground sm:w-56 shrink-0">Silence Threshold</span>
|
||||
<div className="flex items-center gap-2 w-fit">
|
||||
{!isMobile && <input type="range" min={-60} max={-20} step={1} value={sttSilenceThresholdDb} onChange={(e) => setSttSilenceThresholdDb(Number(e.target.value))} className={sliderClass} />}
|
||||
<span className="typography-ui-label text-foreground tabular-nums min-w-[3.5rem] text-right">
|
||||
{sttSilenceThresholdDb} dB
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-8 py-0.5">
|
||||
<span className="typography-ui-label text-foreground sm:w-56 shrink-0">Silence Hold</span>
|
||||
<div className="flex items-center gap-2 w-fit">
|
||||
{!isMobile && <input type="range" min={500} max={3000} step={100} value={sttSilenceHoldMs} onChange={(e) => setSttSilenceHoldMs(Number(e.target.value))} className={sliderClass} />}
|
||||
<NumberInput value={sttSilenceHoldMs} onValueChange={setSttSilenceHoldMs} min={500} max={3000} step={100} className="w-20 tabular-nums" />
|
||||
<span className="typography-meta text-muted-foreground">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Playback & Summarization */}
|
||||
<div className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
|
||||
Reference in New Issue
Block a user