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
|
|
@ -43,6 +43,8 @@ import type {
|
|||
|
||||
const logger = getLogger('MeetingModeService')
|
||||
|
||||
let isShowingMeetingSaveDialog = false
|
||||
|
||||
interface MeetingModeServiceEvents {
|
||||
'state-changed': (state: MeetingModeState) => void
|
||||
'segment': (segment: CaptionSegment) => void
|
||||
|
|
@ -93,13 +95,55 @@ class MeetingModeService extends EventEmitter {
|
|||
return this._meetingModeActive
|
||||
}
|
||||
|
||||
/** 활성 세션, 자막, 오디오 캡처 상태를 강제로 완전 초기화 */
|
||||
async forceReset(): Promise<void> {
|
||||
logger.warn('MeetingModeService: forceReset 실행')
|
||||
if (this._audioLevelTimer) {
|
||||
clearInterval(this._audioLevelTimer)
|
||||
this._audioLevelTimer = null
|
||||
}
|
||||
if (this._audioDataHandler) {
|
||||
try {
|
||||
const { getAudioCaptureService } = await import('./AudioCaptureService')
|
||||
getAudioCaptureService().off('audio-data', this._audioDataHandler)
|
||||
} catch { /* noop */ }
|
||||
this._audioDataHandler = null
|
||||
}
|
||||
try {
|
||||
const { getCaptionService } = await import('./CaptionService')
|
||||
const captionService = getCaptionService()
|
||||
if (this._segmentHandler) {
|
||||
captionService.off('segment', this._segmentHandler)
|
||||
this._segmentHandler = null
|
||||
}
|
||||
await captionService.stop().catch(() => {})
|
||||
} catch { /* noop */ }
|
||||
|
||||
try {
|
||||
const { getAudioCaptureService } = await import('./AudioCaptureService')
|
||||
await getAudioCaptureService().stop().catch(() => {})
|
||||
} catch { /* noop */ }
|
||||
|
||||
this._meetingModeActive = false
|
||||
this._sessionId = null
|
||||
this._sessionStartedAt = null
|
||||
this._segments = []
|
||||
this._memos = []
|
||||
this._setState('idle')
|
||||
this._sendStateToRenderer()
|
||||
}
|
||||
|
||||
// ── 녹음 시작 ──
|
||||
|
||||
async startRecording(): Promise<MeetingStartResult> {
|
||||
async startRecording(options?: { force?: boolean }): Promise<MeetingStartResult> {
|
||||
if (options?.force) {
|
||||
await this.forceReset()
|
||||
}
|
||||
|
||||
if (this._state !== 'idle') {
|
||||
throw new D3ROError(
|
||||
ErrorCode.MeetingAlreadyRecording,
|
||||
`회의 모드가 이미 활성 상태입니다: ${this._state}`,
|
||||
`회의 모드가 이미 실행 중입니다 (${this._state}). 이전 회의를 종료하거나 강제 초기화 후 다시 시도해주세요.`,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -108,10 +152,14 @@ class MeetingModeService extends EventEmitter {
|
|||
const captionService = getCaptionService()
|
||||
const captionState = captionService.getState()
|
||||
if (captionState !== 'inactive') {
|
||||
throw new D3ROError(
|
||||
ErrorCode.MeetingAlreadyRecording,
|
||||
'자막 모드가 활성 상태입니다. 먼저 자막을 종료해주세요.',
|
||||
)
|
||||
if (options?.force) {
|
||||
await captionService.stop().catch(() => {})
|
||||
} else {
|
||||
throw new D3ROError(
|
||||
ErrorCode.MeetingAlreadyRecording,
|
||||
'실시간 자막 모드가 아직 실행 중입니다. 자막을 먼저 종료하거나 강제 초기화 후 시작해주세요.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this._setState('recording')
|
||||
|
|
@ -569,6 +617,39 @@ class MeetingModeService extends EventEmitter {
|
|||
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.PROCESSING_PROGRESS, progress)
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 문서/폴리시/채팅/화자추정 LLM.
|
||||
* llmBackend==='local' 이면 LocalLLM(Ollama). online 이면 Premium, 불가 시 Local 폴백.
|
||||
*/
|
||||
private async _resolveMeetingLlm(): Promise<{
|
||||
generate: (
|
||||
text: string,
|
||||
options?: { systemPrompt?: string; temperature?: number; maxTokens?: number },
|
||||
) => Promise<{ text: string; model?: string }>
|
||||
chatStream: (
|
||||
messages: Array<{ role: string; content: string }>,
|
||||
options?: { temperature?: number },
|
||||
) => AsyncGenerator<string, string>
|
||||
}> {
|
||||
const backend = configGet('llmBackend')
|
||||
if (backend === 'online') {
|
||||
try {
|
||||
const { getPremiumLLMService } = await import('./PremiumLLMService')
|
||||
const premium = getPremiumLLMService()
|
||||
if (premium.isAvailable()) {
|
||||
return premium
|
||||
}
|
||||
logger.warn('Premium LLM unavailable for meeting — falling back to local')
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Premium LLM init failed for meeting: ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
return getLocalLLMService()
|
||||
}
|
||||
|
||||
// ── Phase 14.5: 전사 수정 ──
|
||||
|
||||
updateTranscript(sessionId: string, editedTranscript: string): void {
|
||||
|
|
@ -626,8 +707,7 @@ class MeetingModeService extends EventEmitter {
|
|||
|
||||
sendProgress(10)
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llmService = getLocalLLMService()
|
||||
const llmService = await this._resolveMeetingLlm()
|
||||
|
||||
sendProgress(30)
|
||||
|
||||
|
|
@ -759,11 +839,26 @@ class MeetingModeService extends EventEmitter {
|
|||
break
|
||||
}
|
||||
|
||||
const { filePath } = await dialog.showSaveDialog({
|
||||
title: '문서 내보내기',
|
||||
defaultPath: defaultName,
|
||||
filters,
|
||||
})
|
||||
if (isShowingMeetingSaveDialog) {
|
||||
return ''
|
||||
}
|
||||
isShowingMeetingSaveDialog = true
|
||||
let filePath: string | undefined
|
||||
try {
|
||||
const { getMainWindow } = await import('../windows/WindowManager')
|
||||
const mainWindow = getMainWindow()
|
||||
const dialogOptions = {
|
||||
title: '문서 내보내기',
|
||||
defaultPath: defaultName,
|
||||
filters,
|
||||
}
|
||||
const res = mainWindow
|
||||
? await dialog.showSaveDialog(mainWindow, dialogOptions)
|
||||
: await dialog.showSaveDialog(dialogOptions)
|
||||
filePath = res.filePath
|
||||
} finally {
|
||||
isShowingMeetingSaveDialog = false
|
||||
}
|
||||
if (!filePath) return ''
|
||||
|
||||
switch (format) {
|
||||
|
|
@ -780,14 +875,12 @@ class MeetingModeService extends EventEmitter {
|
|||
const html = `<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: 'Malgun Gothic', sans-serif; margin: 40px; color: #222; }
|
||||
h1 { font-size: 22px; border-bottom: 2px solid #f25b29; padding-bottom: 8px; }
|
||||
h2 { font-size: 16px; color: #444; margin-top: 24px; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; font-size: 13px; }
|
||||
th { background: #f5f5f5; font-weight: 600; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; padding: 40px; color: #222; }
|
||||
h1 { font-size: 20px; border-bottom: 1px solid #ddd; padding-bottom: 8px; }
|
||||
h2 { font-size: 16px; margin-top: 20px; }
|
||||
p { line-height: 1.6; }
|
||||
ul { padding-left: 20px; }
|
||||
li { margin: 4px 0; }
|
||||
</style>
|
||||
|
|
@ -916,8 +1009,7 @@ class MeetingModeService extends EventEmitter {
|
|||
const transcript = row.rawTranscript
|
||||
if (!transcript) throw new D3ROError(ErrorCode.MeetingPolishFailed, '전사 텍스트가 없습니다')
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
const llm = await this._resolveMeetingLlm()
|
||||
const result = await llm.generate(transcript, {
|
||||
systemPrompt: '다음 음성 전사 텍스트를 다듬어주세요. 필러 단어(음, 어, 그, 아 등)를 제거하고, 문장 구조를 자연스럽게 교정하되, 원래 의미와 내용은 절대 변경하지 마세요. 타임스탬프 형식 [MM:SS]은 그대로 유지하세요.',
|
||||
temperature: 0.3,
|
||||
|
|
@ -950,8 +1042,7 @@ ${transcript}`
|
|||
|
||||
this._chatHistory.push({ role: 'user', content: userMessage })
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
const llm = await this._resolveMeetingLlm()
|
||||
|
||||
const messages = [
|
||||
{ role: 'system' as const, content: systemPrompt },
|
||||
|
|
@ -1102,8 +1193,7 @@ ${transcript}`
|
|||
): Promise<void> {
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.DIARIZATION_PROGRESS, { sessionId, percent: 30 })
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
const llm = await this._resolveMeetingLlm()
|
||||
|
||||
const transcript = row.editedTranscript ?? row.rawTranscript ?? ''
|
||||
const speakerHint = numSpeakers && numSpeakers > 0
|
||||
|
|
@ -1171,3 +1261,8 @@ export function getMeetingModeService(): MeetingModeService {
|
|||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
export function resetMeetingModeServiceForTests(): void {
|
||||
if (instance) instance.removeAllListeners()
|
||||
instance = null
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue