centralize mainheader listen session logic
This commit is contained in:
parent
e86c2db464
commit
bcefa75154
@ -2,7 +2,9 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
||||
|
||||
export class MainHeader extends LitElement {
|
||||
static properties = {
|
||||
isSessionActive: { type: Boolean, state: true },
|
||||
// isSessionActive: { type: Boolean, state: true },
|
||||
isTogglingSession: { type: Boolean, state: true },
|
||||
actionText: { type: String, state: true },
|
||||
shortcuts: { type: Object, state: true },
|
||||
};
|
||||
|
||||
@ -95,6 +97,11 @@ export class MainHeader extends LitElement {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listen-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.listen-button.active::before {
|
||||
background: rgba(215, 0, 0, 0.5);
|
||||
}
|
||||
@ -103,6 +110,24 @@ export class MainHeader extends LitElement {
|
||||
background: rgba(255, 20, 20, 0.6);
|
||||
}
|
||||
|
||||
.listen-button.done {
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.listen-button.done .action-text-content {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.listen-button.done .listen-icon svg rect,
|
||||
.listen-button.done .listen-icon svg path {
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.listen-button.done:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.listen-button:hover::before {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
@ -132,6 +157,38 @@ export class MainHeader extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.listen-button.done::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.4s infinite ease-in-out both;
|
||||
}
|
||||
.loading-dots span:nth-of-type(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.loading-dots span:nth-of-type(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 80%, 100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
40% {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
-webkit-app-region: no-drag;
|
||||
height: 26px;
|
||||
@ -242,7 +299,9 @@ export class MainHeader extends LitElement {
|
||||
this.isAnimating = false;
|
||||
this.hasSlidIn = false;
|
||||
this.settingsHideTimer = null;
|
||||
this.isSessionActive = false;
|
||||
// this.isSessionActive = false;
|
||||
this.isTogglingSession = false;
|
||||
this.actionText = 'Listen';
|
||||
this.animationEndTimer = null;
|
||||
this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
|
||||
}
|
||||
@ -305,10 +364,19 @@ export class MainHeader extends LitElement {
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
this._sessionStateListener = (event, { isActive }) => {
|
||||
this.isSessionActive = isActive;
|
||||
|
||||
this._sessionStateTextListener = (event, text) => {
|
||||
this.actionText = text;
|
||||
this.isTogglingSession = false;
|
||||
};
|
||||
ipcRenderer.on('session-state-changed', this._sessionStateListener);
|
||||
ipcRenderer.on('session-state-text', this._sessionStateTextListener);
|
||||
|
||||
|
||||
// this._sessionStateListener = (event, { isActive }) => {
|
||||
// this.isSessionActive = isActive;
|
||||
// this.isTogglingSession = false;
|
||||
// };
|
||||
// ipcRenderer.on('session-state-changed', this._sessionStateListener);
|
||||
this._shortcutListener = (event, keybinds) => {
|
||||
console.log('[MainHeader] Received updated shortcuts:', keybinds);
|
||||
this.shortcuts = keybinds;
|
||||
@ -328,9 +396,12 @@ export class MainHeader extends LitElement {
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (this._sessionStateListener) {
|
||||
ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
|
||||
if (this._sessionStateTextListener) {
|
||||
ipcRenderer.removeListener('session-state-text', this._sessionStateTextListener);
|
||||
}
|
||||
// if (this._sessionStateListener) {
|
||||
// ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
|
||||
// }
|
||||
if (this._shortcutListener) {
|
||||
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
|
||||
}
|
||||
@ -341,6 +412,7 @@ export class MainHeader extends LitElement {
|
||||
if (window.require) {
|
||||
window.require('electron').ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
// return Promise.resolve();
|
||||
}
|
||||
|
||||
showSettingsWindow(element) {
|
||||
@ -369,6 +441,23 @@ export class MainHeader extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async _handleListenClick() {
|
||||
if (this.isTogglingSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTogglingSession = true;
|
||||
|
||||
try {
|
||||
const channel = 'toggle-feature';
|
||||
const args = ['listen'];
|
||||
await this.invoke(channel, ...args);
|
||||
} catch (error) {
|
||||
console.error('IPC invoke for session toggle failed:', error);
|
||||
this.isTogglingSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderShortcut(accelerator) {
|
||||
if (!accelerator) return html``;
|
||||
@ -394,31 +483,45 @@ export class MainHeader extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const buttonClasses = {
|
||||
active: this.actionText === 'Stop',
|
||||
done: this.actionText === 'Done',
|
||||
};
|
||||
const showStopIcon = this.actionText === 'Stop' || this.actionText === 'Done';
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<button
|
||||
class="listen-button ${this.isSessionActive ? 'active' : ''}"
|
||||
@click=${() => this.invoke(this.isSessionActive ? 'close-session' : 'toggle-feature', 'listen')}
|
||||
class="listen-button ${Object.keys(buttonClasses).filter(k => buttonClasses[k]).join(' ')}"
|
||||
@click=${this._handleListenClick}
|
||||
?disabled=${this.isTogglingSession}
|
||||
>
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">${this.isSessionActive ? 'Stop' : 'Listen'}</div>
|
||||
</div>
|
||||
<div class="listen-icon">
|
||||
${this.isSessionActive
|
||||
? html`
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="9" height="9" rx="1" fill="white"/>
|
||||
</svg>
|
||||
|
||||
`
|
||||
: html`
|
||||
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.69922 2.7515C1.69922 2.37153 2.00725 2.0635 2.38722 2.0635H2.73122C3.11119 2.0635 3.41922 2.37153 3.41922 2.7515V8.2555C3.41922 8.63547 3.11119 8.9435 2.73122 8.9435H2.38722C2.00725 8.9435 1.69922 8.63547 1.69922 8.2555V2.7515Z" fill="white"/>
|
||||
<path d="M5.13922 1.3755C5.13922 0.995528 5.44725 0.6875 5.82722 0.6875H6.17122C6.55119 0.6875 6.85922 0.995528 6.85922 1.3755V9.6315C6.85922 10.0115 6.55119 10.3195 6.17122 10.3195H5.82722C5.44725 10.3195 5.13922 10.0115 5.13922 9.6315V1.3755Z" fill="white"/>
|
||||
<path d="M8.57922 3.0955C8.57922 2.71553 8.88725 2.4075 9.26722 2.4075H9.61122C9.99119 2.4075 10.2992 2.71553 10.2992 3.0955V7.9115C10.2992 8.29147 9.99119 8.5995 9.61122 8.5995H9.26722C8.88725 8.5995 8.57922 8.29147 8.57922 7.9115V3.0955Z" fill="white"/>
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
${this.isTogglingSession
|
||||
? html`
|
||||
<div class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">${this.actionText}</div>
|
||||
</div>
|
||||
<div class="listen-icon">
|
||||
${showStopIcon
|
||||
? html`
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="9" height="9" rx="1" fill="white"/>
|
||||
</svg>
|
||||
`
|
||||
: html`
|
||||
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.69922 2.7515C1.69922 2.37153 2.00725 2.0635 2.38722 2.0635H2.73122C3.11119 2.0635 3.41922 2.37153 3.41922 2.7515V8.2555C3.41922 8.63547 3.11119 8.9435 2.73122 8.9435H2.38722C2.00725 8.9435 1.69922 8.63547 1.69922 8.2555V2.7515Z" fill="white"/>
|
||||
<path d="M5.13922 1.3755C5.13922 0.995528 5.44725 0.6875 5.82722 0.6875H6.17122C6.55119 0.6875 6.85922 0.995528 6.85922 1.3755V9.6315C6.85922 10.0115 6.55119 10.3195 6.17122 10.3195H5.82722C5.44725 10.3195 5.13922 10.0115 5.13922 9.6315V1.3755Z" fill="white"/>
|
||||
<path d="M8.57922 3.0955C8.57922 2.71553 8.88725 2.4075 9.26722 2.4075H9.61122C9.99119 2.4075 10.2992 2.71553 10.2992 3.0955V7.9115C10.2992 8.29147 9.99119 8.5995 9.61122 8.5995H9.26722C8.88725 8.5995 8.57922 8.29147 8.57922 7.9115V3.0955Z" fill="white"/>
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
`}
|
||||
</button>
|
||||
|
||||
<div class="header-actions ask-action" @click=${() => this.invoke('toggle-feature', 'ask')}>
|
||||
|
@ -472,7 +472,7 @@ function createWindows() {
|
||||
createFeatureWindows(windowPool.get('header'));
|
||||
}
|
||||
|
||||
|
||||
const header = windowPool.get('header');
|
||||
if (featureName === 'listen') {
|
||||
console.log(`[WindowManager] Toggling feature: ${featureName}`);
|
||||
const listenWindow = windowPool.get(featureName);
|
||||
@ -480,18 +480,22 @@ function createWindows() {
|
||||
if (listenService && listenService.isSessionActive()) {
|
||||
console.log('[WindowManager] Listen session is active, closing it via toggle.');
|
||||
await listenService.closeSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (listenWindow.isVisible()) {
|
||||
listenWindow.webContents.send('window-hide-animation');
|
||||
listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
header.webContents.send('session-state-text', 'Done');
|
||||
// return;
|
||||
} else {
|
||||
listenWindow.show();
|
||||
updateLayout();
|
||||
// listenWindow.webContents.send('start-listening-session');
|
||||
listenWindow.webContents.send('window-show-animation');
|
||||
await listenService.initializeSession();
|
||||
// listenWindow.webContents.send('start-listening-session');
|
||||
if (listenWindow.isVisible()) {
|
||||
listenWindow.webContents.send('window-hide-animation');
|
||||
listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
header.webContents.send('session-state-text', 'Listen');
|
||||
} else {
|
||||
listenWindow.show();
|
||||
updateLayout();
|
||||
listenWindow.webContents.send('window-show-animation');
|
||||
await listenService.initializeSession();
|
||||
listenWindow.webContents.send('session-state-changed', { isActive: true });
|
||||
header.webContents.send('session-state-text', 'Stop');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,9 +144,7 @@ class ListenService {
|
||||
|
||||
console.log('✅ Listen service initialized successfully.');
|
||||
|
||||
this.sendToRenderer('session-state-changed', { isActive: true });
|
||||
this.sendToRenderer('update-status', 'Connected. Ready to listen.');
|
||||
// this.sendToRenderer('change-listen-capture-state', { status: "start" });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@ -181,6 +179,7 @@ class ListenService {
|
||||
|
||||
async closeSession() {
|
||||
try {
|
||||
this.sendToRenderer('change-listen-capture-state', { status: "stop" });
|
||||
// Close STT sessions
|
||||
await this.sttService.closeSessions();
|
||||
|
||||
@ -194,9 +193,7 @@ class ListenService {
|
||||
this.currentSessionId = null;
|
||||
this.summaryService.resetConversationHistory();
|
||||
|
||||
this.sendToRenderer('session-state-changed', { isActive: false });
|
||||
this.sendToRenderer('session-did-close');
|
||||
this.sendToRenderer('change-listen-capture-state', { status: "stop" });
|
||||
|
||||
console.log('Listen service session closed.');
|
||||
return { success: true };
|
||||
@ -285,9 +282,9 @@ class ListenService {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('close-session', async () => {
|
||||
return await this.closeSession();
|
||||
});
|
||||
// ipcMain.handle('close-session', async () => {
|
||||
// return await this.closeSession();
|
||||
// });
|
||||
|
||||
ipcMain.handle('update-google-search-setting', async (event, enabled) => {
|
||||
try {
|
||||
|
@ -7,15 +7,9 @@ let aecPtr = 0; // Rust Aec* 1개만 재사용
|
||||
|
||||
/** WASM 모듈 가져오고 1회 초기화 */
|
||||
async function getAec () {
|
||||
if (aecModPromise) {
|
||||
console.log('[AEC] getAec: 캐시=있음(재사용)');
|
||||
return aecModPromise; // 캐시
|
||||
}
|
||||
|
||||
console.log('[AEC] getAec: 캐시=없음 → 모듈 로드 시작');
|
||||
if (aecModPromise) return aecModPromise; // 캐시
|
||||
|
||||
aecModPromise = createAecModule().then((M) => {
|
||||
console.log('[AEC] WASM 모듈 로드 완료');
|
||||
aecMod = M;
|
||||
// C 심볼 → JS 래퍼 바인딩 (딱 1번)
|
||||
M.newPtr = M.cwrap('AecNew', 'number',
|
||||
@ -24,12 +18,7 @@ async function getAec () {
|
||||
['number','number','number','number','number']);
|
||||
M.destroy = M.cwrap('AecDestroy', null, ['number']);
|
||||
return M;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[AEC] WASM 모듈 로드 실패:', err);
|
||||
throw err; // 상위에서도 잡을 수 있게
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return aecModPromise;
|
||||
}
|
||||
@ -143,10 +132,6 @@ function disposeAec () {
|
||||
}
|
||||
|
||||
function runAecSync (micF32, sysF32) {
|
||||
const modStat = aecMod?.HEAPU8 ? '있음' : '없음'; // aecMod가 초기화되었고 HEAP 접근 가능?
|
||||
const ptrStat = aecPtr ? '있음' : '없음'; // newPtr 호출 여부
|
||||
const heapStat = aecMod?.HEAPU8 ? '있음' : '없음'; // HEAPU8 생성 여부
|
||||
console.log(`[AEC] mod:${modStat} ptr:${ptrStat} heap:${heapStat}`);
|
||||
if (!aecMod || !aecPtr || !aecMod.HEAPU8) return micF32; // 아직 모듈 안 뜸 → 패스
|
||||
|
||||
const len = micF32.length;
|
||||
@ -160,7 +145,6 @@ function runAecSync (micF32, sysF32) {
|
||||
const outF32 = float32FromInt16View(new Int16Array(heapBuf, out, len));
|
||||
|
||||
aecMod._free(mic.ptr); aecMod._free(echo.ptr); aecMod._free(out);
|
||||
console.log(`[AEC] 적용 완료`);
|
||||
return outF32;
|
||||
}
|
||||
|
||||
@ -282,7 +266,7 @@ async function setupMicProcessing(micStream) {
|
||||
micProcessor.onaudioprocess = (e) => {
|
||||
const inputData = e.inputBuffer.getChannelData(0);
|
||||
audioBuffer.push(...inputData);
|
||||
// console.log('🎤 micProcessor.onaudioprocess');
|
||||
console.log('🎤 micProcessor.onaudioprocess');
|
||||
|
||||
// samplesPerChunk(=2400) 만큼 모이면 전송
|
||||
while (audioBuffer.length >= samplesPerChunk) {
|
||||
@ -296,7 +280,7 @@ async function setupMicProcessing(micStream) {
|
||||
|
||||
// **음성 구간일 때만 런**
|
||||
processedChunk = runAecSync(new Float32Array(chunk), sysF32);
|
||||
// console.log('🔊 Applied WASM-AEC (speex)');
|
||||
console.log('🔊 Applied WASM-AEC (speex)');
|
||||
} else {
|
||||
console.log('🔊 No system audio for AEC reference');
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user