Compare commits
No commits in common. "6766cb8c8ef27834ea43a91538528c15ca9f2fff" and "57c17d0977afe7400e5cae285a42834678a7e96a" have entirely different histories.
6766cb8c8e
...
57c17d0977
26 changed files with 99 additions and 160 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -13,17 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Cloud-optional backup (encrypted, opt-in)
|
- Cloud-optional backup (encrypted, opt-in)
|
||||||
- Plugin system for custom pipelines
|
- Plugin system for custom pipelines
|
||||||
|
|
||||||
## [1.3.6] - 2026-09-18
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Installing the local speech engine always failed** with a hash mismatch, leaving local
|
|
||||||
transcription unusable. The downloaded parts were checked against bytes counted from the
|
|
||||||
network stream, while the joined archive was checked against what was actually written to
|
|
||||||
disk, so a truncated write passed part verification and only surfaced later as an archive
|
|
||||||
mismatch with no usable diagnostic. Every check now reads the file on disk, the joined
|
|
||||||
archive is size-checked before hashing, mismatch errors report the actual and expected
|
|
||||||
values, and a failed part is discarded and retried up to three times.
|
|
||||||
|
|
||||||
## [1.3.5] - 2026-09-18
|
## [1.3.5] - 2026-09-18
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"info": {
|
"info": {
|
||||||
"title": "D3RO-VOICE Admin API",
|
"title": "D3RO-VOICE Admin API",
|
||||||
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
|
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
|
||||||
"version": "1.3.6"
|
"version": "1.3.5"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/admin",
|
"name": "@d3ro/admin",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
|
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>1.3.6</Version>
|
<Version>1.3.5</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/desktop",
|
"name": "@d3ro/desktop",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"productName": "d3ro-voice",
|
"productName": "d3ro-voice",
|
||||||
"description": "로컬 AI 음성 어시스턴트 (Electron)",
|
"description": "로컬 AI 음성 어시스턴트 (Electron)",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
import { EventEmitter, once } from 'events'
|
import { EventEmitter, once } from 'events'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs'
|
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs'
|
||||||
import { mkdir, rm, stat } from 'node:fs/promises'
|
import { mkdir, rm } from 'node:fs/promises'
|
||||||
import { Readable, Writable } from 'node:stream'
|
import { Readable } from 'node:stream'
|
||||||
import { pipeline } from 'node:stream/promises'
|
import { pipeline } from 'node:stream/promises'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
|
@ -69,8 +69,6 @@ export interface RuntimeStatus {
|
||||||
|
|
||||||
const RUNTIME_DIR_NAME = 'runtime'
|
const RUNTIME_DIR_NAME = 'runtime'
|
||||||
const DOWNLOAD_TIMEOUT_MS = 120_000
|
const DOWNLOAD_TIMEOUT_MS = 120_000
|
||||||
/** 부품 다운로드 재시도 횟수 — 전송 중 잘림/일시적 네트워크 오류 대비 */
|
|
||||||
const PART_DOWNLOAD_ATTEMPTS = 3
|
|
||||||
|
|
||||||
class RuntimeProvisioner extends EventEmitter {
|
class RuntimeProvisioner extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -250,99 +248,66 @@ class RuntimeProvisioner extends EventEmitter {
|
||||||
|
|
||||||
for (const part of entry.parts) {
|
for (const part of entry.parts) {
|
||||||
const partPath = join(tempDir, part.name)
|
const partPath = join(tempDir, part.name)
|
||||||
const partSize = await this._downloadPart(part, partPath)
|
const response = await fetch(part.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new D3ROError(
|
||||||
|
ErrorCode.STTSidecarSpawnFailed,
|
||||||
|
`런타임 부품을 받을 수 없습니다 (HTTP ${response.status}): ${part.name}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
downloadedBytes += partSize
|
const hash = createHash('sha256')
|
||||||
const elapsed = Math.max(0.001, (Date.now() - startedAt) / 1000)
|
let partBytes = 0
|
||||||
this._emitProgress(
|
const source = Readable.fromWeb(response.body as never)
|
||||||
component,
|
source.on('data', (chunk: Buffer) => {
|
||||||
'downloading',
|
hash.update(chunk)
|
||||||
totalBytes > 0 ? Math.min(100, Math.round((downloadedBytes * 100) / totalBytes)) : 0,
|
partBytes += chunk.length
|
||||||
downloadedBytes,
|
downloadedBytes += chunk.length
|
||||||
totalBytes,
|
const elapsed = Math.max(0.001, (Date.now() - startedAt) / 1000)
|
||||||
Math.round(downloadedBytes / elapsed),
|
this._emitProgress(
|
||||||
)
|
component,
|
||||||
}
|
'downloading',
|
||||||
|
totalBytes > 0 ? Math.min(100, Math.round((downloadedBytes * 100) / totalBytes)) : 0,
|
||||||
|
downloadedBytes,
|
||||||
|
totalBytes,
|
||||||
|
Math.round(downloadedBytes / elapsed),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// 부품을 순서대로 이어 붙인다 (스트리밍 — 메모리에 통째로 올리지 않는다)
|
await pipeline(source, createWriteStream(partPath))
|
||||||
const archiveStream = createWriteStream(archivePath)
|
|
||||||
for (const part of entry.parts) {
|
|
||||||
await pipeline(createReadStream(join(tempDir, part.name)), archiveStream, { end: false })
|
|
||||||
}
|
|
||||||
archiveStream.end()
|
|
||||||
await once(archiveStream, 'finish')
|
|
||||||
|
|
||||||
// 크기를 먼저 본다 — 불일치하면 "어디까지 받았는지"가 로그에 남아 진단이 가능하다.
|
const actual = hash.digest('hex')
|
||||||
const archiveSize = (await stat(archivePath)).size
|
if (part.sha256 && actual !== part.sha256) {
|
||||||
const expectedSize = entry.parts.reduce((sum, part) => sum + part.size, 0)
|
throw new D3ROError(
|
||||||
if (archiveSize !== expectedSize) {
|
ErrorCode.STTSidecarSpawnFailed,
|
||||||
throw new D3ROError(
|
`런타임 부품 해시 불일치 (${part.name})`,
|
||||||
ErrorCode.STTSidecarSpawnFailed,
|
)
|
||||||
`런타임 아카이브 크기 불일치 (${component}: ${archiveSize} != ${expectedSize})`,
|
}
|
||||||
)
|
if (partBytes !== part.size) {
|
||||||
}
|
throw new D3ROError(
|
||||||
|
ErrorCode.STTSidecarSpawnFailed,
|
||||||
const actualArchive = await sha256File(archivePath)
|
`런타임 부품 크기 불일치 (${part.name}: ${partBytes} != ${part.size})`,
|
||||||
if (entry.sha256 && actualArchive !== entry.sha256) {
|
|
||||||
throw new D3ROError(
|
|
||||||
ErrorCode.STTSidecarSpawnFailed,
|
|
||||||
`런타임 아카이브 해시 불일치 (${component}: ${actualArchive} != ${entry.sha256})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 부품 하나를 디스크로 내려받고 디스크 기준으로 크기·해시를 검증한다.
|
|
||||||
* 전송이 도중에 끊기면 같은 부품을 다시 받는다 (기존에는 1회 실패가 곧 설치 실패였다).
|
|
||||||
*/
|
|
||||||
private async _downloadPart(part: RuntimePart, partPath: string): Promise<number> {
|
|
||||||
let lastError: Error | null = null
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= PART_DOWNLOAD_ATTEMPTS; attempt += 1) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(part.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })
|
|
||||||
if (!response.ok || !response.body) {
|
|
||||||
throw new D3ROError(
|
|
||||||
ErrorCode.STTSidecarSpawnFailed,
|
|
||||||
`런타임 부품을 받을 수 없습니다 (HTTP ${response.status}): ${part.name}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스트림을 파일로 저장한 뒤 "디스크에 실제로 남은 파일"에서 크기와 해시를 계산한다.
|
|
||||||
// 메모리 스트림에서 센 값으로 검증하면, 디스크 쓰기가 잘려도 부품 검사를 통과해
|
|
||||||
// 결합 단계에 가서야 해시 불일치로 터진다 — 실측 사고.
|
|
||||||
await pipeline(Readable.fromWeb(response.body as never), createWriteStream(partPath))
|
|
||||||
|
|
||||||
const partSize = (await stat(partPath)).size
|
|
||||||
if (partSize !== part.size) {
|
|
||||||
throw new D3ROError(
|
|
||||||
ErrorCode.STTSidecarSpawnFailed,
|
|
||||||
`런타임 부품 크기 불일치 (${part.name}: ${partSize} != ${part.size})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualPartHash = await sha256File(partPath)
|
|
||||||
if (part.sha256 && actualPartHash !== part.sha256) {
|
|
||||||
throw new D3ROError(
|
|
||||||
ErrorCode.STTSidecarSpawnFailed,
|
|
||||||
`런타임 부품 해시 불일치 (${part.name})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return partSize
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err instanceof Error ? err : new Error(String(err))
|
|
||||||
await rm(partPath, { force: true }).catch(() => undefined)
|
|
||||||
logger.warn(
|
|
||||||
`런타임 부품 다운로드 실패 (${part.name}, ${attempt}/${PART_DOWNLOAD_ATTEMPTS}): ${lastError.message}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError ?? new D3ROError(
|
// 부품을 순서대로 이어 인다 (스트리밍 — 메모리에 통째로 올리지 않는다)
|
||||||
ErrorCode.STTSidecarSpawnFailed,
|
const archiveHash = createHash('sha256')
|
||||||
`런타임 부품 다운로드 실패 (${part.name})`,
|
const archiveStream = createWriteStream(archivePath)
|
||||||
)
|
for (const part of entry.parts) {
|
||||||
|
const source = createReadStream(join(tempDir, part.name))
|
||||||
|
source.on('data', (chunk: string | Buffer) => archiveHash.update(chunk))
|
||||||
|
await pipeline(source, archiveStream, { end: false })
|
||||||
|
}
|
||||||
|
archiveStream.end()
|
||||||
|
await once(archiveStream, 'finish')
|
||||||
|
const actualArchive = archiveHash.digest('hex')
|
||||||
|
if (entry.sha256 && actualArchive !== entry.sha256) {
|
||||||
|
throw new D3ROError(
|
||||||
|
ErrorCode.STTSidecarSpawnFailed,
|
||||||
|
`런타임 아카이브 해시 불일치 (${component})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _emitProgress(
|
private _emitProgress(
|
||||||
|
|
@ -364,18 +329,6 @@ class RuntimeProvisioner extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 파일 SHA-256 (스트림 — 메모리에 통째로 올리지 않는다) */
|
|
||||||
async function sha256File(path: string): Promise<string> {
|
|
||||||
const hash = createHash('sha256')
|
|
||||||
await pipeline(createReadStream(path), new Writable({
|
|
||||||
write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
|
|
||||||
hash.update(chunk)
|
|
||||||
callback()
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
return hash.digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
let _instance: RuntimeProvisioner | null = null
|
let _instance: RuntimeProvisioner | null = null
|
||||||
|
|
||||||
export function getRuntimeProvisioner(): RuntimeProvisioner {
|
export function getRuntimeProvisioner(): RuntimeProvisioner {
|
||||||
|
|
@ -387,4 +340,4 @@ export function getRuntimeProvisioner(): RuntimeProvisioner {
|
||||||
|
|
||||||
export function resetRuntimeProvisionerForTests(): void {
|
export function resetRuntimeProvisionerForTests(): void {
|
||||||
_instance = null
|
_instance = null
|
||||||
}
|
}
|
||||||
|
|
@ -151,8 +151,8 @@ def versionSettingsValid = configuredVersionName != null &&
|
||||||
configuredVersionName ==~ strictSemver &&
|
configuredVersionName ==~ strictSemver &&
|
||||||
configuredVersionCodeValue != null &&
|
configuredVersionCodeValue != null &&
|
||||||
configuredVersionCodeValue <= 2100000000L
|
configuredVersionCodeValue <= 2100000000L
|
||||||
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.6"
|
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.5"
|
||||||
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031006
|
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031005
|
||||||
|
|
||||||
def requiredReleaseSettings = [
|
def requiredReleaseSettings = [
|
||||||
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
|
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1031006;
|
CURRENT_PROJECT_VERSION = 1031005;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
|
|
@ -265,7 +265,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.6;
|
MARKETING_VERSION = 1.3.5;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
|
@ -287,14 +287,14 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1031006;
|
CURRENT_PROJECT_VERSION = 1031005;
|
||||||
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
INFOPLIST_FILE = D3ROVoice/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.6;
|
MARKETING_VERSION = 1.3.5;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Stability improvements.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
안정성을 개선했습니다.
|
|
||||||
14
apps/mobile-rn/package-lock.json
generated
14
apps/mobile-rn/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/mobile-rn",
|
"name": "@d3ro/mobile-rn",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@d3ro/mobile-rn",
|
"name": "@d3ro/mobile-rn",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/api-client": "file:../../packages/api-client",
|
"@d3ro/api-client": "file:../../packages/api-client",
|
||||||
"@d3ro/core": "file:../../packages/core",
|
"@d3ro/core": "file:../../packages/core",
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
},
|
},
|
||||||
"../..": {
|
"../..": {
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/desktop",
|
"apps/desktop",
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/api-client": {
|
"../../packages/api-client": {
|
||||||
"name": "@d3ro/api-client",
|
"name": "@d3ro/api-client",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/core": {
|
"../../packages/core": {
|
||||||
"name": "@d3ro/core",
|
"name": "@d3ro/core",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"docx": "^9.6.1"
|
"docx": "^9.6.1"
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/i18n": {
|
"../../packages/i18n": {
|
||||||
"name": "@d3ro/i18n",
|
"name": "@d3ro/i18n",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
},
|
},
|
||||||
"../../packages/ui-native": {
|
"../../packages/ui-native": {
|
||||||
"name": "@d3ro/ui-native",
|
"name": "@d3ro/ui-native",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/mobile-rn",
|
"name": "@d3ro/mobile-rn",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/web",
|
"name": "@d3ro/web",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스",
|
"description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export function Sidebar(): React.ReactElement {
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
|
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
|
||||||
v1.3.6
|
v1.3.5
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
|
||||||
| INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 was published to Forgejo on 2026-09-15; product version moved to `1.2.0` as a forward-fix with CI-only publication, a same-version re-release guard, and download centers that link the feed instead of repository paths. `1.3.0` (2026-09-18) carries the local-STT fixes; Windows publication still needs the CI signing secrets (`11` GAP-REL-02). |
|
| INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 was published to Forgejo on 2026-09-15; product version moved to `1.2.0` as a forward-fix with CI-only publication, a same-version re-release guard, and download centers that link the feed instead of repository paths. `1.3.0` (2026-09-18) carries the local-STT fixes; Windows publication still needs the CI signing secrets (`11` GAP-REL-02). |
|
||||||
| INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. |
|
| INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. |
|
||||||
| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. |
|
| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. |
|
||||||
| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 검증은 전부 디스크에 기록된 파일 기준이며(부품 크기·해시 → 결합본 크기·해시), 부품 다운로드는 최대 3회 재시도한다. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/18초, ffmpeg 21.7MiB/5초). |
|
| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/17초, ffmpeg 21.7MiB/5초). |
|
||||||
| INFRA-19 | 네이티브 ABI + updater 설정 게이트 | [x] | `scripts/ci/verify-native-abi.mjs`(패키징된 `better_sqlite3.node`가 Electron ABI인지 호스트 Node 로드 거부로 판별) + `scripts/ci/fix-native-abi.mjs`(로컬 잠금 우회용 주입). GitLab/Forgejo/GitHub 패키징 단계에 검증 삽입. 2026-09-18: Node ABI 모듈로 앱이 시작 즉시 죽은 사고 + 누락으로 자동 업데이트가 죽은 사고를 함께 방지(). |
|
| INFRA-19 | 네이티브 ABI + updater 설정 게이트 | [x] | `scripts/ci/verify-native-abi.mjs`(패키징된 `better_sqlite3.node`가 Electron ABI인지 호스트 Node 로드 거부로 판별) + `scripts/ci/fix-native-abi.mjs`(로컬 잠금 우회용 주입). GitLab/Forgejo/GitHub 패키징 단계에 검증 삽입. 2026-09-18: Node ABI 모듈로 앱이 시작 즉시 죽은 사고 + 누락으로 자동 업데이트가 죽은 사고를 함께 방지(). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
|
||||||
| GAP-REL-04 | Release | canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 **100MiB**를 넘으면 HTTP 413으로 거부한다. | `scripts/ci/publish-updater-release.mjs`, `docs/deployment/unsigned-distribution.md` | `[~]` 2026-09-18: 설치본을 90.6MiB로 줄여 업데이트 피드 게시를 복구했다(GAP-STT-07). 휴대용/Scoop 채널은 여전히 95MiB 분할이 필요하다. |
|
| GAP-REL-04 | Release | canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 **100MiB**를 넘으면 HTTP 413으로 거부한다. | `scripts/ci/publish-updater-release.mjs`, `docs/deployment/unsigned-distribution.md` | `[~]` 2026-09-18: 설치본을 90.6MiB로 줄여 업데이트 피드 게시를 복구했다(GAP-STT-07). 휴대용/Scoop 채널은 여전히 95MiB 분할이 필요하다. |
|
||||||
| GAP-STT-07 | Local STT | 진(사이드카)을 앱 번들에 넣으면 설치본이 100MiB를 넘고 매 업데이트마다 162MiB를 다시 받는다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts`, `apps/desktop/electron-builder.yml` | `[x]` 2026-09-18: 설치본에서 엔진/ffmpeg를 제거하고 처음 필요할 때 `runtime-latest`에서 내려받는다(부품별 + 결합본 SHA-256 검증). 설치본 189MB → 90.6MiB, 런타임 1회 116MiB(엔진 94.4 + ffmpeg 21.7). 실제 feed로 통합 검증 완료. |
|
| GAP-STT-07 | Local STT | 진(사이드카)을 앱 번들에 넣으면 설치본이 100MiB를 넘고 매 업데이트마다 162MiB를 다시 받는다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts`, `apps/desktop/electron-builder.yml` | `[x]` 2026-09-18: 설치본에서 엔진/ffmpeg를 제거하고 처음 필요할 때 `runtime-latest`에서 내려받는다(부품별 + 결합본 SHA-256 검증). 설치본 189MB → 90.6MiB, 런타임 1회 116MiB(엔진 94.4 + ffmpeg 21.7). 실제 feed로 통합 검증 완료. |
|
||||||
| GAP-STT-06 | Local STT | Decode settings were untuned: previous-text conditioning let repeated hallucinations compound, and no VAD parameters meant slow, uneven segments. | `apps/desktop/sidecar/main.py` | `[x]` 2026-09-18: `condition_on_previous_text=false`, bounded low-temperature fallback, `no_speech`/`compression_ratio`/`log_prob` thresholds, 300 ms silence trimming. Same transcript, ~5x faster on the reference machine (7.7 s audio: 1609 ms → 303 ms). |
|
| GAP-STT-06 | Local STT | Decode settings were untuned: previous-text conditioning let repeated hallucinations compound, and no VAD parameters meant slow, uneven segments. | `apps/desktop/sidecar/main.py` | `[x]` 2026-09-18: `condition_on_previous_text=false`, bounded low-temperature fallback, `no_speech`/`compression_ratio`/`log_prob` thresholds, 300 ms silence trimming. Same transcript, ~5x faster on the reference machine (7.7 s audio: 1609 ms → 303 ms). |
|
||||||
| GAP-STT-08 | Local STT | 1.3.5 설치본에서 엔진 설치가 "런타임 아카이브 해시 불일치 (sidecar)"로 항상 실패했다. 부품 검증은 **메모리 스트림**에서 센 값으로, 결합 검증은 **디스크 파일**에서 계산해 기준이 서로 달랐다. 디스크 쓰기가 잘려도 부품 검사를 통과하고 결합 단계에서만 터지므로 원인 파악도 불가능했다. 재시도가 없어 전송이 한 번 끊기면 곧바로 설치 실패였다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts` | `[x]` 2026-09-18: 부품 크기·해시를 디스크 파일 기준으로 통일하고, 결합본은 크기를 먼저 검사한 뒤 해시를 본다(오류 메시지에 실제/기대값 포함). 부품 다운로드는 실패 시 해당 파일을 지우고 최대 3회 재시도한다. 서버 아티팩트는 무결함을 확인했고(부품 2개 해시 일치, 결합본 `e203aa53…` = 인덱스 기대값), 실제 feed로 설치를 재현해 18초 만에 성공. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/desktop",
|
"apps/desktop",
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
},
|
},
|
||||||
"apps/admin": {
|
"apps/admin": {
|
||||||
"name": "@d3ro/admin",
|
"name": "@d3ro/admin",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/api-client": "*",
|
"@d3ro/api-client": "*",
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
},
|
},
|
||||||
"apps/desktop": {
|
"apps/desktop": {
|
||||||
"name": "@d3ro/desktop",
|
"name": "@d3ro/desktop",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@d3ro/web",
|
"name": "@d3ro/web",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/api-client": "*",
|
"@d3ro/api-client": "*",
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -16861,7 +16861,7 @@
|
||||||
},
|
},
|
||||||
"packages/api-client": {
|
"packages/api-client": {
|
||||||
"name": "@d3ro/api-client",
|
"name": "@d3ro/api-client",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
|
@ -16878,7 +16878,7 @@
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@d3ro/core",
|
"name": "@d3ro/core",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"docx": "^9.6.1"
|
"docx": "^9.6.1"
|
||||||
|
|
@ -16889,7 +16889,7 @@
|
||||||
},
|
},
|
||||||
"packages/i18n": {
|
"packages/i18n": {
|
||||||
"name": "@d3ro/i18n",
|
"name": "@d3ro/i18n",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
|
|
@ -16900,7 +16900,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@d3ro/ui",
|
"name": "@d3ro/ui",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*"
|
"@d3ro/core": "*"
|
||||||
|
|
@ -16920,7 +16920,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui-native": {
|
"packages/ui-native": {
|
||||||
"name": "@d3ro/ui-native",
|
"name": "@d3ro/ui-native",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-monorepo",
|
"name": "d3ro-voice-monorepo",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)",
|
"description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)",
|
||||||
"author": "D3RO",
|
"author": "D3RO",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/api-client",
|
"name": "@d3ro/api-client",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)",
|
"description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/core",
|
"name": "@d3ro/core",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸",
|
"description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/i18n",
|
"name": "@d3ro/i18n",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸",
|
"description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/ui-native",
|
"name": "@d3ro/ui-native",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
|
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@d3ro/ui",
|
"name": "@d3ro/ui",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵",
|
"description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"androidVersionCode": 1031006,
|
"androidVersionCode": 1031005,
|
||||||
"iosBuildNumber": 1031006,
|
"iosBuildNumber": 1031005,
|
||||||
"releaseDate": "2026-09-18",
|
"releaseDate": "2026-09-18",
|
||||||
"desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f"
|
"desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
site/package-lock.json
generated
4
site/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-site",
|
"name": "d3ro-voice-site",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "d3ro-voice-site",
|
"name": "d3ro-voice-site",
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "d3ro-voice-site",
|
"name": "d3ro-voice-site",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.6",
|
"version": "1.3.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 5199 --host",
|
"dev": "vite --port 5199 --host",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue