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
|
|
@ -93,6 +93,36 @@ payloads.push({
|
|||
contentType: 'text/plain',
|
||||
})
|
||||
|
||||
// ── 로컬 AI 런타임 번들 게시 (설치본에는 없고, 앱이 처음 필요할 때 내려받는다) ──
|
||||
const runtimeDir = join(releaseDir, 'runtime')
|
||||
const runtimePayloads = []
|
||||
const runtimeIndexPath = join(runtimeDir, 'runtime.json')
|
||||
if (existsSync(runtimeIndexPath)) {
|
||||
const runtimeIndex = JSON.parse(readFileSync(runtimeIndexPath, 'utf8'))
|
||||
for (const [component, entry] of Object.entries(runtimeIndex.components ?? {})) {
|
||||
for (const part of entry.parts) {
|
||||
const partPath = join(runtimeDir, part.name)
|
||||
if (!existsSync(partPath)) {
|
||||
console.error(`[portable] 런타임 부품이 없습니다: ${partPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const bytes = await readFile(partPath)
|
||||
const sha = createHash('sha256').update(bytes).digest('hex')
|
||||
if (sha !== part.sha256) {
|
||||
console.error(`[portable] 런타임 부품 sha256 불일치: ${part.name}`)
|
||||
process.exit(1)
|
||||
}
|
||||
runtimePayloads.push({ name: part.name, bytes, contentType: 'application/octet-stream' })
|
||||
}
|
||||
void component
|
||||
}
|
||||
runtimePayloads.push({
|
||||
name: 'runtime.json',
|
||||
bytes: Buffer.from(`${JSON.stringify(runtimeIndex, null, 2)}\n`, 'utf8'),
|
||||
contentType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
const authorization = forgejoAuthorization()
|
||||
const bases = [`${FEED}/portable-${version}`, `${FEED}/portable-latest`]
|
||||
|
||||
|
|
@ -142,6 +172,13 @@ async function upload(url, body, contentType) {
|
|||
console.log(`[portable] uploaded ${url}`)
|
||||
}
|
||||
|
||||
const runtimeBases = [`${FEED}/runtime-${version}`, `${FEED}/runtime-latest`]
|
||||
for (const base of runtimeBases) {
|
||||
for (const payload of runtimePayloads) {
|
||||
await upload(`${base}/${encodeURIComponent(payload.name)}`, payload.bytes, payload.contentType)
|
||||
}
|
||||
}
|
||||
|
||||
for (const base of bases) {
|
||||
for (const payload of payloads) {
|
||||
await upload(`${base}/${encodeURIComponent(payload.name)}`, payload.bytes, payload.contentType)
|
||||
|
|
@ -155,6 +192,9 @@ console.log(
|
|||
` 볼륨 : ${index.volumeCount}개 / 합계 ${(index.totalSize / 1048576).toFixed(1)}MiB`,
|
||||
` 인덱스 : ${FEED}/portable-latest/portable.json`,
|
||||
` 스크립트: ${FEED}/portable-latest/install-d3ro-voice.ps1`,
|
||||
runtimePayloads.length
|
||||
? ` 런타임 : ${FEED}/runtime-latest/runtime.json (${runtimePayloads.length}개 파일)`
|
||||
: ' 런타임 : (없음)',
|
||||
'',
|
||||
' 설치(Scoop, 권장):',
|
||||
' scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue