// Caption Overlay 팝업 스크립트 // Phase 10.1: Live Caption — 실시간 자막 오버레이 // Vanilla JS (다른 팝업과 동일한 패턴) ;(function () { 'use strict' // ── 설정 (기본값, IPC로 업데이트) ───────────────────── var config = { fontSize: 18, opacity: 0.85, maxLines: 3, autoClearMs: 5000 } // ── DOM 참조 ───────────────────────────────────────── var linesContainer = document.getElementById('lines') var container = document.getElementById('container') // ── i18n ────────────────────────────────────────────── var i18nStrings = {} // ── 상태 ────────────────────────────────────────────── /** @type {Array<{el: HTMLElement, timer: number|null, id: string}>} */ var lines = [] /** @type {HTMLElement|null} */ var deltaLine = null // ── 자막 줄 추가 ────────────────────────────────────── /** * 확정된 자막 세그먼트를 추가한다. * @param {{id: string, text: string, timestamp: number, isFinal: boolean}} segment */ function addSegment(segment) { // delta 줄이 있으면 제거 (확정 줄로 교체) removeDeltaLine() var el = document.createElement('div') el.className = 'caption-line' el.textContent = segment.text el.style.fontSize = config.fontSize + 'px' linesContainer.appendChild(el) // auto-clear 타이머 설정 var timer = setTimeout(function () { fadeAndRemoveLine(entry) }, config.autoClearMs) var entry = { el: el, timer: timer, id: segment.id } lines.push(entry) // maxLines 초과 시 가장 오래된 줄 제거 while (lines.length > config.maxLines) { var oldest = lines.shift() if (oldest) { if (oldest.timer) clearTimeout(oldest.timer) if (oldest.el.parentNode) { oldest.el.parentNode.removeChild(oldest.el) } } } } /** * 중간(delta) 자막을 업데이트한다 (아직 확정되지 않은 줄). * @param {{text: string, isFinal: boolean}} data */ function updateDelta(data) { if (data.isFinal) { // isFinal이면 segment로 처리 removeDeltaLine() return } if (!deltaLine) { deltaLine = document.createElement('div') deltaLine.className = 'caption-line delta' deltaLine.style.fontSize = config.fontSize + 'px' linesContainer.appendChild(deltaLine) } deltaLine.textContent = data.text } /** * delta 줄을 제거한다. */ function removeDeltaLine() { if (deltaLine && deltaLine.parentNode) { deltaLine.parentNode.removeChild(deltaLine) } deltaLine = null } /** * 줄을 페이드 아웃 후 제거한다. * @param {{el: HTMLElement, timer: number|null, id: string}} entry */ function fadeAndRemoveLine(entry) { entry.el.classList.add('fading') setTimeout(function () { if (entry.el.parentNode) { entry.el.parentNode.removeChild(entry.el) } var idx = lines.indexOf(entry) if (idx !== -1) { lines.splice(idx, 1) } }, 500) // CSS transition 시간과 일치 } /** * 모든 줄을 제거한다. */ function clearAllLines() { for (var i = 0; i < lines.length; i++) { if (lines[i].timer) clearTimeout(lines[i].timer) if (lines[i].el.parentNode) { lines[i].el.parentNode.removeChild(lines[i].el) } } lines = [] removeDeltaLine() } /** * 설정을 적용한다. * @param {object} newConfig */ function applyConfig(newConfig) { if (newConfig.fontSize !== undefined) config.fontSize = newConfig.fontSize if (newConfig.opacity !== undefined) config.opacity = newConfig.opacity if (newConfig.maxLines !== undefined) config.maxLines = newConfig.maxLines if (newConfig.autoClearMs !== undefined) config.autoClearMs = newConfig.autoClearMs // 기존 줄에 폰트 크기 반영 for (var i = 0; i < lines.length; i++) { lines[i].el.style.fontSize = config.fontSize + 'px' } if (deltaLine) { deltaLine.style.fontSize = config.fontSize + 'px' } // 컨테이너 투명도 if (container) { container.style.opacity = String(config.opacity) } } // ── IPC 리스너 ──────────────────────────────────────── if (window.popupAPI) { // 확정된 자막 세그먼트 window.popupAPI.on('caption:segment', function (segment) { addSegment(segment) }) // 중간 전사 결과 (delta) window.popupAPI.on('caption:delta', function (data) { updateDelta(data) }) // 설정 업데이트 window.popupAPI.on('caption:config', function (newConfig) { applyConfig(newConfig) }) // 숨기기 (세션 종료) window.popupAPI.on('caption:hide', function () { clearAllLines() }) // 상태 변경 window.popupAPI.on('caption:stateChanged', function (data) { if (data._i18n) i18nStrings = data._i18n if (data.state === 'starting') { // 로딩 표시 clearAllLines() var loadingEl = document.createElement('div') loadingEl.className = 'caption-line loading' loadingEl.id = 'caption-loading' loadingEl.textContent = i18nStrings.captionLoading || '⏳ Loading STT model...' loadingEl.style.fontSize = config.fontSize + 'px' linesContainer.appendChild(loadingEl) } else if (data.state === 'active') { // 로딩 표시 제거 var existing = document.getElementById('caption-loading') if (existing && existing.parentNode) { existing.parentNode.removeChild(existing) } } else if (data.state === 'inactive' || data.state === 'stopping') { clearAllLines() } }) } })()