feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
597
apps/desktop/src/renderer/components/STTTab.tsx
Normal file
597
apps/desktop/src/renderer/components/STTTab.tsx
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
// src/renderer/components/STTTab.tsx
|
||||
// STT 서비스 선택 및 다중 AI 공급자(OpenAI, Groq, Deepgram, AssemblyAI, Google 등) 연동 탭
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Button,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Divider,
|
||||
Paper,
|
||||
Stack,
|
||||
Chip,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Alert,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
ChevronDown,
|
||||
Key,
|
||||
HelpCircle,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Cpu,
|
||||
Cloud,
|
||||
Zap,
|
||||
Sparkles,
|
||||
RefreshCw,
|
||||
HardDriveDownload,
|
||||
} from 'lucide-react'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '@d3ro/ui/theme'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
import type {
|
||||
AppConfig,
|
||||
STTProviderType,
|
||||
STTProviderInfo,
|
||||
STTProviderConfig,
|
||||
STTModel,
|
||||
} from '@d3ro/core/types'
|
||||
import { CodexOAuthGuideModal } from './CodexOAuthGuideModal'
|
||||
|
||||
interface STTTabProps {
|
||||
config: Partial<AppConfig>
|
||||
updateConfig: (key: keyof AppConfig, value: AppConfig[keyof AppConfig]) => void
|
||||
}
|
||||
|
||||
export function STTTab({ config, updateConfig }: STTTabProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [providers, setProviders] = useState<STTProviderInfo[]>([])
|
||||
const [activeProvider, setActiveProvider] = useState<STTProviderType>(config.sttProvider || 'local')
|
||||
const [providerConfig, setProviderConfig] = useState<STTProviderConfig>({})
|
||||
const [showApiKey, setShowApiKey] = useState(false)
|
||||
const [guideOpen, setGuideOpen] = useState(false)
|
||||
|
||||
// Local models
|
||||
const [localModels, setLocalModels] = useState<STTModel[]>([])
|
||||
const [downloadingModelId, setDownloadingModelId] = useState<string | null>(null)
|
||||
const [downloadPercent, setDownloadPercent] = useState<number>(0)
|
||||
|
||||
// Test connection state
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; latencyMs: number; message: string } | null>(null)
|
||||
|
||||
// Load providers and local models
|
||||
useEffect(() => {
|
||||
window.electronAPI.stt.getProviders().then((res) => {
|
||||
if (res.success && res.data) setProviders(res.data)
|
||||
})
|
||||
window.electronAPI.stt.getActiveProvider().then((res) => {
|
||||
if (res.success && res.data) setActiveProvider(res.data)
|
||||
})
|
||||
window.electronAPI.stt.getModels().then((res) => {
|
||||
if (res.success && res.data) setLocalModels(res.data)
|
||||
})
|
||||
|
||||
const unsubDownload = window.electronAPI.stt.onDownloadProgress((e) => {
|
||||
setDownloadingModelId(e.modelId)
|
||||
setDownloadPercent(e.percent)
|
||||
if (e.percent >= 100) {
|
||||
setDownloadingModelId(null)
|
||||
window.electronAPI.stt.getModels().then((res) => {
|
||||
if (res.success && res.data) setLocalModels(res.data)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubDownload()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load specific provider config when activeProvider changes
|
||||
useEffect(() => {
|
||||
if (!activeProvider) return
|
||||
window.electronAPI.stt.getProviderConfig({ provider: activeProvider }).then((res) => {
|
||||
if (res.success && res.data) {
|
||||
setProviderConfig(res.data)
|
||||
}
|
||||
})
|
||||
setTestResult(null)
|
||||
}, [activeProvider])
|
||||
|
||||
const handleProviderChange = useCallback(
|
||||
(provider: STTProviderType) => {
|
||||
setActiveProvider(provider)
|
||||
updateConfig('sttProvider', provider)
|
||||
window.electronAPI.stt.setProvider({ provider })
|
||||
},
|
||||
[updateConfig]
|
||||
)
|
||||
|
||||
const handleProviderConfigFieldChange = useCallback(
|
||||
(field: keyof STTProviderConfig, value: unknown) => {
|
||||
const updated = { ...providerConfig, [field]: value }
|
||||
setProviderConfig(updated)
|
||||
window.electronAPI.stt.setProviderConfig({
|
||||
provider: activeProvider,
|
||||
config: updated,
|
||||
})
|
||||
},
|
||||
[activeProvider, providerConfig]
|
||||
)
|
||||
|
||||
const handleTestConnection = useCallback(async () => {
|
||||
setTesting(true)
|
||||
setTestResult(null)
|
||||
try {
|
||||
const res = await window.electronAPI.stt.testConnection({
|
||||
provider: activeProvider,
|
||||
apiKey: providerConfig.apiKey,
|
||||
baseUrl: providerConfig.baseUrl,
|
||||
modelId: providerConfig.modelId,
|
||||
})
|
||||
if (res.success && res.data) {
|
||||
setTestResult(res.data)
|
||||
}
|
||||
} finally {
|
||||
setTesting(false)
|
||||
}
|
||||
}, [activeProvider, providerConfig])
|
||||
|
||||
const handleDownloadModel = useCallback(async (modelId: string) => {
|
||||
setDownloadingModelId(modelId)
|
||||
setDownloadPercent(0)
|
||||
await window.electronAPI.stt.downloadModel({ modelId })
|
||||
}, [])
|
||||
|
||||
const currentProviderInfo = providers.find((p) => p.id === activeProvider)
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
{/* ── 1. 공급자 선택 헤더 ── */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
STT 음성 인식 서비스 선택
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
startIcon={<HelpCircle size={14} />}
|
||||
onClick={() => setGuideOpen(true)}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
color: d3roPalette.accent.main,
|
||||
py: 0.2,
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
OpenAI & Codex OAuth 설정 안내
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* ── 공급자 카드 그리드 ── */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))',
|
||||
gap: 1.2,
|
||||
}}
|
||||
>
|
||||
{providers.map((p) => {
|
||||
const isSelected = p.id === activeProvider
|
||||
return (
|
||||
<Paper
|
||||
key={p.id}
|
||||
elevation={0}
|
||||
onClick={() => handleProviderChange(p.id)}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
cursor: 'pointer',
|
||||
borderRadius: d3roRadius.sm,
|
||||
bgcolor: isSelected ? d3roPalette.bg.elevated : d3roPalette.bg.inset,
|
||||
border: isSelected ? `1.5px solid ${d3roPalette.accent.main}` : `1px solid ${d3roPalette.border.subtle}`,
|
||||
boxShadow: isSelected ? d3roShadow.elevated : 'none',
|
||||
transition: 'all 120ms ease-out',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
'&:hover': {
|
||||
bgcolor: d3roPalette.bg.elevated,
|
||||
borderColor: isSelected ? d3roPalette.accent.main : d3roPalette.border.default,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
{p.isCloud ? (
|
||||
p.id === 'groq' ? (
|
||||
<Zap size={15} color={d3roPalette.tag.green} />
|
||||
) : p.id === 'openai' ? (
|
||||
<Sparkles size={15} color={d3roPalette.accent.main} />
|
||||
) : (
|
||||
<Cloud size={15} color={d3roPalette.text.secondary} />
|
||||
)
|
||||
) : (
|
||||
<Cpu size={15} color={d3roPalette.tag.blue} />
|
||||
)}
|
||||
{isSelected && (
|
||||
<CheckCircle2 size={14} color={d3roPalette.accent.main} />
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '12px', mb: 0.2 }}>
|
||||
{p.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={p.badge}
|
||||
size="small"
|
||||
sx={{
|
||||
mt: 1,
|
||||
height: 18,
|
||||
fontSize: '9px',
|
||||
fontWeight: 700,
|
||||
bgcolor: isSelected ? d3roPalette.accent.muted : 'transparent',
|
||||
color: isSelected ? d3roPalette.accent.main : d3roPalette.text.disabled,
|
||||
border: isSelected ? 'none' : `1px solid ${d3roPalette.border.subtle}`,
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── 2. 선택된 공급자 빠른 설정 (Simple Mode) ── */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${d3roPalette.border.default}`,
|
||||
borderRadius: d3roRadius.md,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontFamily: d3roFontMono, color: d3roPalette.accent.main }}>
|
||||
{currentProviderInfo?.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.secondary }}>
|
||||
{currentProviderInfo?.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={currentProviderInfo?.isCloud ? '클라우드 API' : '로컬 오프라인'}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
bgcolor: currentProviderInfo?.isCloud ? d3roPalette.tag.purpleBg : d3roPalette.tag.greenBg,
|
||||
color: currentProviderInfo?.isCloud ? d3roPalette.tag.purple : d3roPalette.tag.green,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Local Whisper 설정 */}
|
||||
{activeProvider === 'local' ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl size="small" fullWidth>
|
||||
<InputLabel>{t('settings.whisperModel')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.whisperModel')}
|
||||
value={config.sttModelId ?? 'large-v3-turbo'}
|
||||
onChange={(e) => {
|
||||
updateConfig('sttModelId', e.target.value)
|
||||
window.electronAPI.stt.setModel({ modelId: e.target.value })
|
||||
}}
|
||||
>
|
||||
{localModels.map((m) => (
|
||||
<MenuItem key={m.id} value={m.id}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
|
||||
<span>{m.name} ({Math.round(m.sizeBytes / 1_000_000)} MB)</span>
|
||||
{m.downloaded ? (
|
||||
<Chip label="설치됨" size="small" sx={{ height: 18, fontSize: '10px', bgcolor: d3roPalette.tag.greenBg, color: d3roPalette.tag.green }} />
|
||||
) : (
|
||||
<Chip label="미다운로드" size="small" sx={{ height: 18, fontSize: '10px', bgcolor: d3roPalette.tag.orangeBg, color: d3roPalette.tag.orange }} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 모델 다운로드 진행 바 또는 다운로드 버튼 */}
|
||||
{(() => {
|
||||
const selected = localModels.find((m) => m.id === (config.sttModelId ?? 'large-v3-turbo'))
|
||||
if (selected && !selected.downloaded) {
|
||||
const isDownloading = downloadingModelId === selected.id
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: d3roPalette.bg.elevated,
|
||||
border: `1px solid ${d3roPalette.border.default}`,
|
||||
borderRadius: d3roRadius.sm,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{selected.name} 모델 다운로드가 필요합니다
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.secondary }}>
|
||||
크기: 약 {Math.round(selected.sizeBytes / 1_000_000)} MB
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={isDownloading}
|
||||
startIcon={isDownloading ? <CircularProgress size={14} /> : <HardDriveDownload size={14} />}
|
||||
onClick={() => handleDownloadModel(selected.id)}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
bgcolor: d3roPalette.accent.main,
|
||||
color: d3roPalette.bg.app,
|
||||
'&:hover': { bgcolor: d3roPalette.accent.hover },
|
||||
}}
|
||||
>
|
||||
{isDownloading ? `다운로드 중 (${downloadPercent}%)` : '모델 다운로드'}
|
||||
</Button>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</Box>
|
||||
) : (
|
||||
/* Cloud STT (OpenAI, Groq, Deepgram, AssemblyAI, Google, Custom) 설정 */
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<TextField
|
||||
label={`${currentProviderInfo?.name} API Key`}
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
value={providerConfig.apiKey ?? ''}
|
||||
onChange={(e) => handleProviderConfigFieldChange('apiKey', e.target.value)}
|
||||
placeholder={
|
||||
activeProvider === 'openai'
|
||||
? 'sk-proj-...'
|
||||
: activeProvider === 'groq'
|
||||
? 'gsk_...'
|
||||
: 'API Key를 입력하세요'
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
InputProps={{
|
||||
startAdornment: <Key size={16} style={{ color: d3roPalette.text.inactive, marginRight: 8 }} />,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size="small" onClick={() => setShowApiKey((v) => !v)} edge="end">
|
||||
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing}
|
||||
startIcon={testing ? <CircularProgress size={14} /> : <RefreshCw size={14} />}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
minWidth: 105,
|
||||
height: 40,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{testing ? '테스트 중...' : '연결 테스트'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Test result feedback banner */}
|
||||
{testResult && (
|
||||
<Alert
|
||||
severity={testResult.success ? 'success' : 'error'}
|
||||
icon={testResult.success ? <CheckCircle2 size={16} /> : <AlertCircle size={16} />}
|
||||
sx={{
|
||||
py: 0.5,
|
||||
px: 1.5,
|
||||
fontSize: '12px',
|
||||
bgcolor: testResult.success ? d3roPalette.tag.greenBg : d3roPalette.tag.redBg,
|
||||
color: testResult.success ? d3roPalette.tag.green : d3roPalette.tag.red,
|
||||
border: `1px solid ${testResult.success ? d3roPalette.tag.green : d3roPalette.tag.red}`,
|
||||
}}
|
||||
>
|
||||
{testResult.message}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* ── 3. 전문가 상세 설정 (Accordion - Collapsed by default) ── */}
|
||||
<Accordion
|
||||
elevation={0}
|
||||
sx={{
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${d3roPalette.border.subtle}`,
|
||||
borderRadius: `${d3roRadius.sm} !important`,
|
||||
'&:before': { display: 'none' },
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ChevronDown size={18} color={d3roPalette.text.inactive} />}
|
||||
sx={{
|
||||
minHeight: 40,
|
||||
py: 0.5,
|
||||
'& .MuiAccordionSummary-content': { my: 0.5 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: d3roTypo.label.size,
|
||||
fontWeight: 700,
|
||||
color: d3roPalette.text.secondary,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
전문가 상세 설정 (엔드포인트, 커스텀 모델, 폴백 옵션)
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails sx={{ pt: 0, pb: 2, px: 2 }}>
|
||||
<Stack spacing={2}>
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle, mb: 1 }} />
|
||||
|
||||
{/* Custom Model ID */}
|
||||
<TextField
|
||||
label="사용할 모델 ID (Model ID)"
|
||||
size="small"
|
||||
value={providerConfig.modelId ?? currentProviderInfo?.defaultModel ?? ''}
|
||||
onChange={(e) => handleProviderConfigFieldChange('modelId', e.target.value)}
|
||||
helperText={`기본값: ${currentProviderInfo?.defaultModel || '자동'}`}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{/* Custom Base URL (for proxies or custom servers) */}
|
||||
{activeProvider !== 'local' && (
|
||||
<TextField
|
||||
label="API 엔드포인트 URL (Base URL)"
|
||||
size="small"
|
||||
value={providerConfig.baseUrl ?? currentProviderInfo?.defaultBaseUrl ?? ''}
|
||||
onChange={(e) => handleProviderConfigFieldChange('baseUrl', e.target.value)}
|
||||
placeholder={currentProviderInfo?.defaultBaseUrl}
|
||||
helperText="프록시 또는 사내 프라이빗 서버를 사용하는 경우 변경하세요."
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fallback to local Whisper toggle */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.sttFallbackToLocal ?? true}
|
||||
onChange={(e) => updateConfig('sttFallbackToLocal', e.target.checked)}
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: d3roPalette.tag.green },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
|
||||
backgroundColor: d3roPalette.tag.green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontSize: d3roTypo.small.size, fontWeight: 600 }}>
|
||||
로컬 Whisper 자동 폴백 (Auto Fallback)
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.inactive }}>
|
||||
클라우드 STT 요청이 실패(인터넷 끊김, 쿼터 초과, 키 오류 등)할 경우 로컬 Whisper로 안전하게 자동 전환합니다.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
|
||||
{/* ── 4. 언어 및 화자 구분 공통 설정 ── */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
음성 인식 공통 설정
|
||||
</Typography>
|
||||
|
||||
<FormControl size="small" fullWidth>
|
||||
<InputLabel>{t('settings.sttLanguage')}</InputLabel>
|
||||
<Select
|
||||
label={t('settings.sttLanguage')}
|
||||
value={config.sttLanguage ?? 'auto'}
|
||||
onChange={(e) => {
|
||||
updateConfig('sttLanguage', e.target.value)
|
||||
window.electronAPI.stt.setLanguage({ language: e.target.value })
|
||||
}}
|
||||
>
|
||||
<MenuItem value="auto">{t('settings.sttLang.auto')}</MenuItem>
|
||||
<MenuItem value="ko">{t('settings.sttLang.ko')}</MenuItem>
|
||||
<MenuItem value="en">{t('settings.sttLang.en')}</MenuItem>
|
||||
<MenuItem value="ja">{t('settings.sttLang.ja')}</MenuItem>
|
||||
<MenuItem value="zh">{t('settings.sttLang.zh')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
{t('settings.diarization')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label={t('settings.hfToken')}
|
||||
type="password"
|
||||
value={(config as Record<string, unknown>)['hfToken'] as string ?? ''}
|
||||
onChange={(e) => updateConfig('hfToken' as keyof AppConfig, e.target.value as never)}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText={t('settings.hfTokenHint')}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={(config as Record<string, unknown>)['diarizationEnabled'] as boolean ?? false}
|
||||
onChange={(e) => updateConfig('diarizationEnabled' as keyof AppConfig, e.target.checked as never)}
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: d3roPalette.tag.purple },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
|
||||
backgroundColor: d3roPalette.tag.purple,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontSize: d3roTypo.small.size }}>{t('settings.diarization')}</Typography>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.inactive, fontSize: '11px' }}>
|
||||
{t('settings.diarizationHint')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Codex OAuth & API Key Setup Guide Modal */}
|
||||
<CodexOAuthGuideModal open={guideOpen} onClose={() => setGuideOpen(false)} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue