Spaces:
Running
Running
File size: 11,369 Bytes
b8cc2bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | import { Component, For, Show } from 'solid-js';
import { appStore } from '../stores/appStore';
import { getModelDisplayName, MODELS } from './ModelLoadingOverlay';
import type { AudioEngine } from '../lib/audio/types';
const formatInterval = (ms: number) => {
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
return `${ms}ms`;
};
export type SettingsPanelSection = 'full' | 'audio' | 'model';
export interface SettingsContentProps {
/** When 'audio' or 'model', only that section is shown (e.g. hover on mic or load button). */
section?: SettingsPanelSection;
onClose: () => void;
onLoadModel: () => void;
onLocalLoad?: (files: FileList) => void;
onOpenDebug: () => void;
onDeviceSelect?: (id: string) => void;
audioEngine?: AudioEngine | null;
/** When true, panel expands upward (bar in lower half); content order is reversed so ASR model stays adjacent to the bar. */
expandUp?: () => boolean;
}
/** Embeddable settings form (e.g. inside floating bar expansion). */
export const SettingsContent: Component<SettingsContentProps> = (props) => {
const isV4 = () => appStore.transcriptionMode() === 'v4-utterance';
const isV3 = () => appStore.transcriptionMode() === 'v3-streaming';
const expandUp = () => props.expandUp?.() ?? false;
const section = () => props.section ?? 'full';
const showAsr = () => section() === 'full' || section() === 'model';
const showAudio = () => section() === 'full' || section() === 'audio';
const showSliders = () => section() === 'full';
const showDebug = () => section() === 'full';
return (
<div class="flex flex-col min-h-0">
<div
class="flex flex-col flex-1 min-h-0 overflow-y-auto p-3 gap-4 custom-scrollbar"
classList={{ 'flex-col-reverse': expandUp() }}
>
<Show when={showAsr()}>
<section class="space-y-2">
<h3 class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">ASR model</h3>
<div class="flex items-center gap-2 flex-wrap">
<select
class="flex-1 min-w-0 text-sm bg-transparent border-b border-[var(--color-earthy-sage)]/40 px-0 py-1.5 text-[var(--color-earthy-dark-brown)] focus:outline-none focus:border-[var(--color-earthy-muted-green)]"
value={appStore.selectedModelId()}
onInput={(e) => appStore.setSelectedModelId((e.target as HTMLSelectElement).value)}
disabled={appStore.modelState() === 'loading'}
>
<For each={MODELS}>
{(m) => <option value={m.id}>{m.name}</option>}
</For>
</select>
<button
type="button"
onClick={props.onLoadModel}
disabled={appStore.modelState() === 'ready' || appStore.modelState() === 'loading'}
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-[var(--color-earthy-muted-green)] hover:bg-[var(--color-earthy-sage)]/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
>
<span class="material-symbols-outlined text-lg">power_settings_new</span>
{appStore.modelState() === 'ready' ? 'Loaded' : appStore.modelState() === 'loading' ? '...' : 'Load'}
</button>
<Show when={props.onLocalLoad}>
<label class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-[var(--color-earthy-soft-brown)] hover:bg-[var(--color-earthy-sage)]/20 transition-colors cursor-pointer shrink-0">
<span class="material-symbols-outlined text-lg">folder_open</span>
Load from file
<input
type="file"
multiple
class="hidden"
accept=".onnx,.bin"
onChange={(e) => {
const files = e.currentTarget.files;
if (files && files.length > 0) props.onLocalLoad?.(files);
e.currentTarget.value = '';
}}
/>
</label>
</Show>
</div>
<p class="text-xs text-[var(--color-earthy-soft-brown)]">
{appStore.modelState() === 'ready' ? getModelDisplayName(appStore.selectedModelId()) : appStore.modelState()}
</p>
<Show when={appStore.modelState() === 'loading'}>
<div class="space-y-1">
<div class="flex justify-between text-xs">
<span>{appStore.modelMessage()}</span>
<span class="font-mono text-[var(--color-earthy-muted-green)]">{Math.round(appStore.modelProgress())}%</span>
</div>
<div class="h-1.5 rounded-full overflow-hidden bg-[var(--color-earthy-sage)]/20">
<div
class="h-full bg-[var(--color-earthy-muted-green)] rounded-full transition-all duration-300"
style={{ width: `${Math.max(0, Math.min(100, appStore.modelProgress()))}%` }}
/>
</div>
</div>
</Show>
</section>
</Show>
<Show when={showAudio()}>
<section class="space-y-2">
<h3 class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">Audio input</h3>
<select
class="w-full text-sm bg-transparent border-b border-[var(--color-earthy-sage)]/40 px-0 py-1.5 text-[var(--color-earthy-dark-brown)] focus:outline-none focus:border-[var(--color-earthy-muted-green)]"
value={appStore.selectedDeviceId()}
onInput={(e) => {
const id = (e.target as HTMLSelectElement).value;
appStore.setSelectedDeviceId(id);
props.onDeviceSelect?.(id);
}}
>
<For each={appStore.availableDevices()}>
{(device) => (
<option value={device.deviceId}>
{device.label || `Device ${device.deviceId.slice(0, 8)}`}
</option>
)}
</For>
</select>
</section>
</Show>
<Show when={showSliders()}>
<section class="grid grid-cols-2 gap-x-4 gap-y-3">
<div class="space-y-1.5 min-w-0">
<div class="flex justify-between items-center gap-2">
<span class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">Energy threshold</span>
<span class="text-sm text-[var(--color-earthy-dark-brown)] tabular-nums shrink-0">{(appStore.energyThreshold() * 100).toFixed(1)}%</span>
</div>
<input
type="range" min="0.005" max="0.3" step="0.005"
value={appStore.energyThreshold()}
onInput={(e) => {
const val = parseFloat(e.currentTarget.value);
appStore.setEnergyThreshold(val);
props.audioEngine?.updateConfig({ energyThreshold: val });
}}
class="debug-slider w-full h-2 rounded-full appearance-none cursor-pointer bg-[var(--color-earthy-sage)]/30"
/>
</div>
<Show when={isV4()}>
<div class="space-y-1.5 min-w-0">
<div class="flex justify-between items-center gap-2">
<span class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">VAD threshold</span>
<span class="text-sm text-[var(--color-earthy-dark-brown)] tabular-nums shrink-0">{(appStore.sileroThreshold() * 100).toFixed(0)}%</span>
</div>
<input
type="range" min="0.1" max="0.9" step="0.05"
value={appStore.sileroThreshold()}
onInput={(e) => appStore.setSileroThreshold(parseFloat(e.currentTarget.value))}
class="debug-slider w-full h-2 rounded-full appearance-none cursor-pointer bg-[var(--color-earthy-sage)]/30"
/>
</div>
<div class="space-y-1.5 min-w-0">
<div class="flex justify-between items-center gap-2">
<span class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">Tick interval</span>
<span class="text-sm text-[var(--color-earthy-dark-brown)] tabular-nums shrink-0">{formatInterval(appStore.v4InferenceIntervalMs())}</span>
</div>
<input
type="range" min="160" max="8000" step="80"
value={appStore.v4InferenceIntervalMs()}
onInput={(e) => appStore.setV4InferenceIntervalMs(parseInt(e.currentTarget.value))}
class="debug-slider w-full h-2 rounded-full appearance-none cursor-pointer bg-[var(--color-earthy-sage)]/30"
/>
<div class="flex justify-between text-[9px] text-[var(--color-earthy-soft-brown)]">
<span>320ms</span>
<span>8.0s</span>
</div>
</div>
<div class="space-y-1.5 min-w-0">
<div class="flex justify-between items-center gap-2">
<span class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">Silence flush</span>
<span class="text-sm text-[var(--color-earthy-dark-brown)] tabular-nums shrink-0">{appStore.v4SilenceFlushSec().toFixed(1)}s</span>
</div>
<input
type="range" min="0.3" max="5.0" step="0.1"
value={appStore.v4SilenceFlushSec()}
onInput={(e) => appStore.setV4SilenceFlushSec(parseFloat(e.currentTarget.value))}
class="debug-slider w-full h-2 rounded-full appearance-none cursor-pointer bg-[var(--color-earthy-sage)]/30"
/>
</div>
</Show>
<Show when={isV3()}>
<div class="space-y-1.5 min-w-0">
<div class="flex justify-between items-center gap-2">
<span class="text-[10px] font-bold uppercase tracking-widest text-[var(--color-earthy-soft-brown)]">Window</span>
<span class="text-sm text-[var(--color-earthy-dark-brown)] tabular-nums shrink-0">{appStore.streamingWindow().toFixed(1)}s</span>
</div>
<input
type="range" min="2.0" max="15.0" step="0.5"
value={appStore.streamingWindow()}
onInput={(e) => appStore.setStreamingWindow(parseFloat(e.currentTarget.value))}
class="debug-slider w-full h-2 rounded-full appearance-none cursor-pointer bg-[var(--color-earthy-sage)]/30"
/>
</div>
</Show>
</section>
</Show>
<Show when={showDebug()}>
<div class="pt-2">
<button
type="button"
onClick={() => {
props.onOpenDebug();
props.onClose();
}}
class="flex items-center gap-2 px-0 py-2 text-sm font-medium text-[var(--color-earthy-muted-green)] hover:opacity-80 transition-opacity w-full"
>
<span class="material-symbols-outlined text-lg">bug_report</span>
Open Debug panel
</button>
</div>
</Show>
</div>
</div>
);
};
|