fix(settings): show the live caption model under the dictation model

The caption model selector now sits directly under the dictation model in
the STT tab's Local Whisper card, where people look for it, and stays in the
General tab's caption section as well; both use one shared component. The
first selector is renamed to 'Dictation model' so the two are told apart.
This commit is contained in:
Yun Chan 2026-09-24 22:08:05 +09:00
parent da0da98285
commit ad6bb70c20
15 changed files with 68 additions and 35 deletions

View file

@ -0,0 +1,41 @@
// 실시간 자막 전용 로컬 Whisper 모델 선택 — STT 탭과 일반 탭 자막 섹션이 함께 쓴다.
import { FormControl, InputLabel, MenuItem, Select, Typography } from '@mui/material'
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
import { useI18n } from '@d3ro/i18n'
import type { STTModel } from '@d3ro/core/types'
/** "받아쓰기와 같게" — 설정에는 null 로 저장한다 */
const SAME_AS_DICTATION = '__same__'
interface CaptionModelSelectProps {
value: string | null | undefined
/** 선택지로 보일 모델 — 받아 둔 모델만 넘긴다 */
models: readonly STTModel[]
onChange: (modelId: string | null) => void
fullWidth?: boolean
}
export function CaptionModelSelect({ value, models, onChange, fullWidth }: CaptionModelSelectProps): React.ReactElement {
const { t } = useI18n()
return (
<FormControl size="small" fullWidth={fullWidth}>
<InputLabel>{t('settings.captionModel')}</InputLabel>
<Select
label={t('settings.captionModel')}
value={value ?? SAME_AS_DICTATION}
onChange={(e) => onChange(e.target.value === SAME_AS_DICTATION ? null : e.target.value)}
>
<MenuItem value={SAME_AS_DICTATION}>{t('settings.captionModel.same')}</MenuItem>
{models.map((m) => (
<MenuItem key={m.id} value={m.id}>
{m.name}
</MenuItem>
))}
</Select>
<Typography sx={{ mt: 0.5, fontSize: d3roTypo.meta.size, color: d3roPalette.text.secondary }}>
{t('settings.captionModel.desc')}
</Typography>
</FormControl>
)
}

View file

@ -50,6 +50,7 @@ import type {
STTModel, STTModel,
} from '@d3ro/core/types' } from '@d3ro/core/types'
import { CodexOAuthGuideModal } from './CodexOAuthGuideModal' import { CodexOAuthGuideModal } from './CodexOAuthGuideModal'
import { CaptionModelSelect } from './CaptionModelSelect'
interface STTTabProps { interface STTTabProps {
config: Partial<AppConfig> config: Partial<AppConfig>
@ -362,6 +363,14 @@ export function STTTab({ config, updateConfig }: STTTabProps): React.ReactElemen
</Select> </Select>
</FormControl> </FormControl>
{/* 실시간 자막 전용 모델 — 1초마다 다시 인식하므로 받아쓰기와 다른 모델이 나을 수 있다 */}
<CaptionModelSelect
fullWidth
value={config.captionSttModelId}
models={localModels.filter((m) => m.downloaded)}
onChange={(modelId) => updateConfig('captionSttModelId', modelId)}
/>
{/* 모델 다운로드 진행 바 또는 다운로드 버튼 */} {/* 모델 다운로드 진행 바 또는 다운로드 버튼 */}
{(() => { {(() => {
const selected = localModels.find((m) => m.id === (config.sttModelId ?? 'large-v3-turbo')) const selected = localModels.find((m) => m.id === (config.sttModelId ?? 'large-v3-turbo'))

View file

@ -28,6 +28,7 @@ import { Led } from '@d3ro/ui/components/ds'
import { LicenseTab } from './LicenseTab' import { LicenseTab } from './LicenseTab'
import { CloudSyncSection } from './CloudSyncSection' import { CloudSyncSection } from './CloudSyncSection'
import { STTTab } from './STTTab' import { STTTab } from './STTTab'
import { CaptionModelSelect } from './CaptionModelSelect'
import { InputConsentPanel } from './input-insights/InputConsentPanel' import { InputConsentPanel } from './input-insights/InputConsentPanel'
import { KeyBindingField } from './keybinding/KeyBindingField' import { KeyBindingField } from './keybinding/KeyBindingField'
import { asTranslationKey } from './keybinding/translation-key' import { asTranslationKey } from './keybinding/translation-key'
@ -64,9 +65,6 @@ const ACTION_GROUP_LABEL_KEYS: Readonly<Record<KeyBindingActionGroup, string>> =
const ACTION_GROUP_ORDER: readonly KeyBindingActionGroup[] = ['voice', 'window', 'input'] const ACTION_GROUP_ORDER: readonly KeyBindingActionGroup[] = ['voice', 'window', 'input']
/** 자막 모델 선택지 "받아쓰기와 같게" — 설정에는 null 로 저장한다 */
const SAME_AS_DICTATION = '__same__'
export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalProps): React.ReactElement { export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalProps): React.ReactElement {
const { t, locale, setLocale } = useI18n() const { t, locale, setLocale } = useI18n()
const [activeTab, setActiveTab] = useState(initialTab) const [activeTab, setActiveTab] = useState(initialTab)
@ -493,26 +491,11 @@ export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalPr
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small"> <CaptionModelSelect
<InputLabel>{t('settings.captionModel')}</InputLabel> value={config.captionSttModelId}
<Select models={downloadedSttModels}
label={t('settings.captionModel')} onChange={(modelId) => updateConfig('captionSttModelId', modelId)}
value={config.captionSttModelId ?? SAME_AS_DICTATION} />
onChange={(e) =>
updateConfig('captionSttModelId', e.target.value === SAME_AS_DICTATION ? null : e.target.value)
}
>
<MenuItem value={SAME_AS_DICTATION}>{t('settings.captionModel.same')}</MenuItem>
{downloadedSttModels.map((m) => (
<MenuItem key={m.id} value={m.id}>
{m.name}
</MenuItem>
))}
</Select>
<Typography sx={{ mt: 0.5, fontSize: d3roTypo.meta.size, color: d3roPalette.text.secondary }}>
{t('settings.captionModel.desc')}
</Typography>
</FormControl>
<FormControlLabel <FormControlLabel
sx={{ mr: 0, alignItems: 'flex-start' }} sx={{ mr: 0, alignItems: 'flex-start' }}

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "Einfügemethode", "settings.insertMethod": "Einfügemethode",
"settings.insertClipboard": "Zwischenablage (Ctrl+V)", "settings.insertClipboard": "Zwischenablage (Ctrl+V)",
"settings.insertKeyboard": "Tastatureingabe", "settings.insertKeyboard": "Tastatureingabe",
"settings.whisperModel": "Whisper-Modell", "settings.whisperModel": "Diktiermodell",
"settings.model.tiny": "tiny (39 MB, am schnellsten)", "settings.model.tiny": "tiny (39 MB, am schnellsten)",
"settings.model.base": "base (74 MB, ausgewogen)", "settings.model.base": "base (74 MB, ausgewogen)",
"settings.model.small": "small (244 MB, gut)", "settings.model.small": "small (244 MB, gut)",

View file

@ -139,7 +139,7 @@
"settings.sttProvider.custom": "Custom (OpenAI-Compatible)", "settings.sttProvider.custom": "Custom (OpenAI-Compatible)",
"settings.sttFallbackToLocal": "Auto Fallback to Local Whisper", "settings.sttFallbackToLocal": "Auto Fallback to Local Whisper",
"settings.sttFallbackToLocalHint": "Automatically switch to offline local Whisper if cloud STT fails", "settings.sttFallbackToLocalHint": "Automatically switch to offline local Whisper if cloud STT fails",
"settings.whisperModel": "Whisper Model", "settings.whisperModel": "Dictation model",
"settings.model.tiny": "tiny (39 MB, fastest)", "settings.model.tiny": "tiny (39 MB, fastest)",
"settings.model.base": "base (74 MB, balanced)", "settings.model.base": "base (74 MB, balanced)",
"settings.model.small": "small (244 MB, good)", "settings.model.small": "small (244 MB, good)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "Método de inserción", "settings.insertMethod": "Método de inserción",
"settings.insertClipboard": "Portapapeles (Ctrl+V)", "settings.insertClipboard": "Portapapeles (Ctrl+V)",
"settings.insertKeyboard": "Escritura con teclado", "settings.insertKeyboard": "Escritura con teclado",
"settings.whisperModel": "Modelo Whisper", "settings.whisperModel": "Modelo de dictado",
"settings.model.tiny": "tiny (39 MB, más rápido)", "settings.model.tiny": "tiny (39 MB, más rápido)",
"settings.model.base": "base (74 MB, equilibrado)", "settings.model.base": "base (74 MB, equilibrado)",
"settings.model.small": "small (244 MB, bueno)", "settings.model.small": "small (244 MB, bueno)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "Méthode d'insertion", "settings.insertMethod": "Méthode d'insertion",
"settings.insertClipboard": "Presse-papiers (Ctrl+V)", "settings.insertClipboard": "Presse-papiers (Ctrl+V)",
"settings.insertKeyboard": "Frappe au clavier", "settings.insertKeyboard": "Frappe au clavier",
"settings.whisperModel": "Modèle Whisper", "settings.whisperModel": "Modèle de dictée",
"settings.model.tiny": "tiny (39 Mo, le plus rapide)", "settings.model.tiny": "tiny (39 Mo, le plus rapide)",
"settings.model.base": "base (74 Mo, équilibré)", "settings.model.base": "base (74 Mo, équilibré)",
"settings.model.small": "small (244 Mo, bon)", "settings.model.small": "small (244 Mo, bon)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "挿入方式", "settings.insertMethod": "挿入方式",
"settings.insertClipboard": "クリップボード (Ctrl+V)", "settings.insertClipboard": "クリップボード (Ctrl+V)",
"settings.insertKeyboard": "キーボード入力", "settings.insertKeyboard": "キーボード入力",
"settings.whisperModel": "Whisperモデル", "settings.whisperModel": "音声入力モデル",
"settings.model.tiny": "tiny (39 MB, 最速)", "settings.model.tiny": "tiny (39 MB, 最速)",
"settings.model.base": "base (74 MB, バランス)", "settings.model.base": "base (74 MB, バランス)",
"settings.model.small": "small (244 MB, 良好)", "settings.model.small": "small (244 MB, 良好)",

View file

@ -147,7 +147,7 @@
"settings.sttProvider.custom": "커스텀 OpenAI 호환 엔드포인트", "settings.sttProvider.custom": "커스텀 OpenAI 호환 엔드포인트",
"settings.sttFallbackToLocal": "로컬 Whisper 자동 폴백", "settings.sttFallbackToLocal": "로컬 Whisper 자동 폴백",
"settings.sttFallbackToLocalHint": "클라우드 STT 실패 시 오프라인 로컬 Whisper로 자동 전환", "settings.sttFallbackToLocalHint": "클라우드 STT 실패 시 오프라인 로컬 Whisper로 자동 전환",
"settings.whisperModel": "Whisper 모델", "settings.whisperModel": "받아쓰기 모델",
"settings.model.tiny": "tiny (39 MB, 가장 빠름)", "settings.model.tiny": "tiny (39 MB, 가장 빠름)",
"settings.model.base": "base (74 MB, 균형)", "settings.model.base": "base (74 MB, 균형)",
"settings.model.small": "small (244 MB, 양호)", "settings.model.small": "small (244 MB, 양호)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "Método de inserção", "settings.insertMethod": "Método de inserção",
"settings.insertClipboard": "Área de transferência (Ctrl+V)", "settings.insertClipboard": "Área de transferência (Ctrl+V)",
"settings.insertKeyboard": "Digitação pelo teclado", "settings.insertKeyboard": "Digitação pelo teclado",
"settings.whisperModel": "Modelo Whisper", "settings.whisperModel": "Modelo de ditado",
"settings.model.tiny": "tiny (39 MB, mais rápido)", "settings.model.tiny": "tiny (39 MB, mais rápido)",
"settings.model.base": "base (74 MB, equilibrado)", "settings.model.base": "base (74 MB, equilibrado)",
"settings.model.small": "small (244 MB, bom)", "settings.model.small": "small (244 MB, bom)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "Метод вставки", "settings.insertMethod": "Метод вставки",
"settings.insertClipboard": "Буфер обмена (Ctrl+V)", "settings.insertClipboard": "Буфер обмена (Ctrl+V)",
"settings.insertKeyboard": "Эмуляция клавиатуры", "settings.insertKeyboard": "Эмуляция клавиатуры",
"settings.whisperModel": "Модель Whisper", "settings.whisperModel": "Модель диктовки",
"settings.model.tiny": "tiny (39 МБ, самая быстрая)", "settings.model.tiny": "tiny (39 МБ, самая быстрая)",
"settings.model.base": "base (74 МБ, баланс)", "settings.model.base": "base (74 МБ, баланс)",
"settings.model.small": "small (244 МБ, хорошая)", "settings.model.small": "small (244 МБ, хорошая)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "วิธีการแทรก", "settings.insertMethod": "วิธีการแทรก",
"settings.insertClipboard": "คลิปบอร์ด (Ctrl+V)", "settings.insertClipboard": "คลิปบอร์ด (Ctrl+V)",
"settings.insertKeyboard": "จำลองการพิมพ์", "settings.insertKeyboard": "จำลองการพิมพ์",
"settings.whisperModel": "โมเดล Whisper", "settings.whisperModel": "โมเดลพิมพ์ด้วยเสียง",
"settings.model.tiny": "tiny (39 MB, เร็วที่สุด)", "settings.model.tiny": "tiny (39 MB, เร็วที่สุด)",
"settings.model.base": "base (74 MB, สมดุล)", "settings.model.base": "base (74 MB, สมดุล)",
"settings.model.small": "small (244 MB, ดี)", "settings.model.small": "small (244 MB, ดี)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "Phương thức chèn", "settings.insertMethod": "Phương thức chèn",
"settings.insertClipboard": "Bộ nhớ tạm (Ctrl+V)", "settings.insertClipboard": "Bộ nhớ tạm (Ctrl+V)",
"settings.insertKeyboard": "Giả lập bàn phím", "settings.insertKeyboard": "Giả lập bàn phím",
"settings.whisperModel": "Mô hình Whisper", "settings.whisperModel": "Mô hình đọc chính tả",
"settings.model.tiny": "tiny (39 MB, nhanh nhất)", "settings.model.tiny": "tiny (39 MB, nhanh nhất)",
"settings.model.base": "base (74 MB, cân bằng)", "settings.model.base": "base (74 MB, cân bằng)",
"settings.model.small": "small (244 MB, tốt)", "settings.model.small": "small (244 MB, tốt)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "插入方式", "settings.insertMethod": "插入方式",
"settings.insertClipboard": "剪貼簿(Ctrl+V)", "settings.insertClipboard": "剪貼簿(Ctrl+V)",
"settings.insertKeyboard": "鍵盤輸入", "settings.insertKeyboard": "鍵盤輸入",
"settings.whisperModel": "Whisper模型", "settings.whisperModel": "聽寫模型",
"settings.model.tiny": "tiny(39 MB,最快)", "settings.model.tiny": "tiny(39 MB,最快)",
"settings.model.base": "base(74 MB,均衡)", "settings.model.base": "base(74 MB,均衡)",
"settings.model.small": "small(244 MB,良好)", "settings.model.small": "small(244 MB,良好)",

View file

@ -99,7 +99,7 @@
"settings.insertMethod": "插入方式", "settings.insertMethod": "插入方式",
"settings.insertClipboard": "剪贴板(Ctrl+V)", "settings.insertClipboard": "剪贴板(Ctrl+V)",
"settings.insertKeyboard": "键盘输入", "settings.insertKeyboard": "键盘输入",
"settings.whisperModel": "Whisper模型", "settings.whisperModel": "听写模型",
"settings.model.tiny": "tiny(39 MB,最快)", "settings.model.tiny": "tiny(39 MB,最快)",
"settings.model.base": "base(74 MB,均衡)", "settings.model.base": "base(74 MB,均衡)",
"settings.model.small": "small(244 MB,良好)", "settings.model.small": "small(244 MB,良好)",