fix(release): restore automatic updates by shipping the speech engine on demand
Some checks failed
deploy-site / deploy (push) Failing after 1m15s
Some checks failed
deploy-site / deploy (push) Failing after 1m15s
Auto-update could not work at all: the installer was 189 MB because it carried the local speech engine and ffmpeg, and the download feed rejects uploads over about 100 MiB, so update metadata could never be published. The installer now leaves those components out and the app fetches them the first time they are needed, verifying every part and the joined archive before installing. The installer is 90.6 MiB, the update feed is published again, and updates stay small because the engine is not re-sent on every release. The fetch is visible and recoverable: the download runs with progress, a failed install cleans up after itself, and Settings > STT shows the runtime status with a manual download action for when the automatic one cannot run.
This commit is contained in:
parent
0411f389d9
commit
0fbbbc1756
42 changed files with 1137 additions and 123 deletions
|
|
@ -69,6 +69,18 @@ export function STTTab({ config, updateConfig }: STTTabProps): React.ReactElemen
|
|||
const [downloadingModelId, setDownloadingModelId] = useState<string | null>(null)
|
||||
const [downloadPercent, setDownloadPercent] = useState<number>(0)
|
||||
|
||||
// 로컬 AI 런타임(사이드카 엔진/ffmpeg) — 설치본에는 없고 필요할 때 내려받는다
|
||||
type RuntimeComponentName = 'sidecar' | 'ffmpeg'
|
||||
interface RuntimeStatusRow {
|
||||
component: RuntimeComponentName
|
||||
installed: boolean
|
||||
path: string
|
||||
sizeBytes: number
|
||||
}
|
||||
const [runtimeStatus, setRuntimeStatus] = useState<RuntimeStatusRow[]>([])
|
||||
const [runtimeBusy, setRuntimeBusy] = useState<RuntimeComponentName | null>(null)
|
||||
const [runtimePercent, setRuntimePercent] = useState(0)
|
||||
|
||||
// Test connection state
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; latencyMs: number; message: string } | null>(null)
|
||||
|
|
@ -96,9 +108,30 @@ export function STTTab({ config, updateConfig }: STTTabProps): React.ReactElemen
|
|||
}
|
||||
})
|
||||
|
||||
const loadRuntime = () => {
|
||||
window.electronAPI.runtime.getStatus().then((res) => {
|
||||
if (res.success && res.data) setRuntimeStatus(res.data)
|
||||
})
|
||||
}
|
||||
loadRuntime()
|
||||
|
||||
const unsubRuntime = window.electronAPI.runtime.onProgress((e) => {
|
||||
if (e.component !== 'sidecar' && e.component !== 'ffmpeg') return
|
||||
if (e.phase === 'done') {
|
||||
setRuntimeBusy(null)
|
||||
setRuntimePercent(100)
|
||||
loadRuntime()
|
||||
return
|
||||
}
|
||||
setRuntimeBusy(e.component)
|
||||
setRuntimePercent(e.percent)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubDownload()
|
||||
}
|
||||
unsubDownload()
|
||||
unsubRuntime()
|
||||
}, [])
|
||||
|
||||
// Load specific provider config when activeProvider changes
|
||||
|
|
@ -133,6 +166,19 @@ export function STTTab({ config, updateConfig }: STTTabProps): React.ReactElemen
|
|||
[activeProvider, providerConfig]
|
||||
)
|
||||
|
||||
const handleEnsureRuntime = useCallback(async (component: RuntimeComponentName) => {
|
||||
setRuntimeBusy(component)
|
||||
setRuntimePercent(0)
|
||||
try {
|
||||
const res = await window.electronAPI.runtime.ensure({ component })
|
||||
if (!res.success) setRuntimePercent(0)
|
||||
} finally {
|
||||
setRuntimeBusy(null)
|
||||
const status = await window.electronAPI.runtime.getStatus()
|
||||
if (status.success && status.data) setRuntimeStatus(status.data)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleTestConnection = useCallback(async () => {
|
||||
setTesting(true)
|
||||
setTestResult(null)
|
||||
|
|
@ -364,6 +410,55 @@ export function STTTab({ config, updateConfig }: STTTabProps): React.ReactElemen
|
|||
}
|
||||
return null
|
||||
})()}
|
||||
|
||||
{/* 로컬 AI 런타임(엔진/ffmpeg): 설치본에는 없고 처음 필요할 때 내려받는다 */}
|
||||
{runtimeStatus.map((row) => {
|
||||
const busy = runtimeBusy === row.component
|
||||
return (
|
||||
<Paper
|
||||
key={row.component}
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: d3roPalette.bg.elevated,
|
||||
border: `1px solid ${d3roPalette.border.default}`,
|
||||
borderRadius: d3roRadius.small,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{row.component === 'sidecar'
|
||||
? '로컬 음성 엔진 (faster-whisper)'
|
||||
: '미디어 변환기 (ffmpeg)'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: d3roPalette.text.secondary }}>
|
||||
{row.installed
|
||||
? `설치됨 · ${Math.round(row.sizeBytes / 1_000_000)} MB`
|
||||
: busy
|
||||
? `다운로드 중 (${runtimePercent}%)`
|
||||
: '설치되지 않음 — 로컬 전사에 필요합니다'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{busy ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant={row.installed ? 'outlined' : 'contained'}
|
||||
startIcon={<HardDriveDownload size={14} />}
|
||||
onClick={() => handleEnsureRuntime(row.component)}
|
||||
sx={{ fontFamily: d3roFontMono, fontSize: '11px' }}
|
||||
>
|
||||
{row.installed ? '다시 설치' : '내려받기'}
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
/* Cloud STT (OpenAI, Groq, Deepgram, AssemblyAI, Google, Custom) 설정 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue