Merge branch 'main' into main
This commit is contained in:
commit
9caa3dc062
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -31,7 +31,6 @@ jobs:
|
|||||||
|
|
||||||
- name: 🖥️ Build Electron app
|
- name: 🖥️ Build Electron app
|
||||||
# Run Electron build script from root directory
|
# Run Electron build script from root directory
|
||||||
|
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: 🚨 Send failure notification to Slack
|
- name: 🚨 Send failure notification to Slack
|
||||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "aec"]
|
||||||
|
path = aec
|
||||||
|
url = https://github.com/samtiz/aec.git
|
1
aec
Submodule
1
aec
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f
|
@ -12,7 +12,7 @@
|
|||||||
"package": "npm run build:renderer && electron-forge package",
|
"package": "npm run build:renderer && electron-forge package",
|
||||||
"make": "npm run build:renderer && electron-forge make",
|
"make": "npm run build:renderer && electron-forge make",
|
||||||
"build": "npm run build:all && electron-builder --config electron-builder.yml --publish never",
|
"build": "npm run build:all && electron-builder --config electron-builder.yml --publish never",
|
||||||
"build:win": "npm run build:all && electron-builder --win --x64 --publish never",
|
"build:win": "npm run build:all && electron-builder --win --x64 --publish never",
|
||||||
"publish": "npm run build:all && electron-builder --config electron-builder.yml --publish always",
|
"publish": "npm run build:all && electron-builder --config electron-builder.yml --publish always",
|
||||||
"lint": "eslint --ext .ts,.tsx,.js .",
|
"lint": "eslint --ext .ts,.tsx,.js .",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
@ -41,7 +41,6 @@
|
|||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.0.0",
|
"dotenv": "^17.0.0",
|
||||||
|
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
@ -76,4 +75,4 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"electron-liquid-glass": "^1.0.1"
|
"electron-liquid-glass": "^1.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
src/assets/aec.js
Normal file
20
src/assets/aec.js
Normal file
File diff suppressed because one or more lines are too long
@ -16,8 +16,17 @@ async function createSTT({ apiKey, language = "en-US", callbacks = {}, ...config
|
|||||||
const lang = language.includes("-") ? language : `${language}-US`
|
const lang = language.includes("-") ? language : `${language}-US`
|
||||||
|
|
||||||
const session = await liveClient.live.connect({
|
const session = await liveClient.live.connect({
|
||||||
model: "gemini-live-2.5-flash-preview",
|
|
||||||
callbacks,
|
model: 'gemini-live-2.5-flash-preview',
|
||||||
|
callbacks: {
|
||||||
|
...callbacks,
|
||||||
|
onMessage: (msg) => {
|
||||||
|
if (!msg || typeof msg !== 'object') return;
|
||||||
|
msg.provider = 'gemini';
|
||||||
|
callbacks.onmessage?.(msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
inputAudioTranscription: {},
|
inputAudioTranscription: {},
|
||||||
speechConfig: { languageCode: lang },
|
speechConfig: { languageCode: lang },
|
||||||
|
@ -72,6 +72,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
|
|||||||
close: () => {
|
close: () => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'session.close' }));
|
ws.send(JSON.stringify({ type: 'session.close' }));
|
||||||
|
ws.onmessage = ws.onerror = () => {}; // 핸들러 제거
|
||||||
ws.close(1000, 'Client initiated close.');
|
ws.close(1000, 'Client initiated close.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,10 +80,17 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const message = JSON.parse(event.data);
|
// ── 종료·하트비트 패킷 필터링 ──────────────────────────────
|
||||||
if (callbacks && callbacks.onmessage) {
|
if (!event.data || event.data === 'null' || event.data === '[DONE]') return;
|
||||||
callbacks.onmessage(message);
|
|
||||||
}
|
let msg;
|
||||||
|
try { msg = JSON.parse(event.data); }
|
||||||
|
catch { return; } // JSON 파싱 실패 무시
|
||||||
|
|
||||||
|
if (!msg || typeof msg !== 'object') return;
|
||||||
|
|
||||||
|
msg.provider = 'openai'; // ← 항상 명시
|
||||||
|
callbacks.onmessage?.(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { BrowserWindow } = require('electron');
|
const { BrowserWindow, app } = require('electron');
|
||||||
const SttService = require('./stt/sttService');
|
const SttService = require('./stt/sttService');
|
||||||
const SummaryService = require('./summary/summaryService');
|
const SummaryService = require('./summary/summaryService');
|
||||||
const authService = require('../../common/services/authService');
|
const authService = require('../../common/services/authService');
|
||||||
@ -117,8 +117,27 @@ class ListenService {
|
|||||||
throw new Error('Failed to initialize database session');
|
throw new Error('Failed to initialize database session');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize STT sessions
|
/* ---------- STT Initialization Retry Logic ---------- */
|
||||||
await this.sttService.initializeSttSessions(language);
|
const MAX_RETRY = 10;
|
||||||
|
const RETRY_DELAY_MS = 300; // 0.3 seconds
|
||||||
|
|
||||||
|
let sttReady = false;
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
||||||
|
try {
|
||||||
|
await this.sttService.initializeSttSessions(language);
|
||||||
|
sttReady = true;
|
||||||
|
break; // Exit on success
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`[ListenService] STT init attempt ${attempt} failed: ${err.message}`
|
||||||
|
);
|
||||||
|
if (attempt < MAX_RETRY) {
|
||||||
|
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sttReady) throw new Error('STT init failed after retries');
|
||||||
|
/* ------------------------------------------- */
|
||||||
|
|
||||||
console.log('✅ Listen service initialized successfully.');
|
console.log('✅ Listen service initialized successfully.');
|
||||||
|
|
||||||
@ -213,9 +232,9 @@ class ListenService {
|
|||||||
try {
|
try {
|
||||||
await this.sendAudioContent(data, mimeType);
|
await this.sendAudioContent(data, mimeType);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error('Error sending user audio:', error);
|
console.error('Error sending user audio:', e);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: e.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,9 +256,13 @@ class ListenService {
|
|||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
return { success: false, error: 'macOS audio capture only available on macOS' };
|
return { success: false, error: 'macOS audio capture only available on macOS' };
|
||||||
}
|
}
|
||||||
|
if (this.sttService.isMacOSAudioRunning?.()) {
|
||||||
|
return { success: false, error: 'already_running' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await this.startMacOSAudioCapture();
|
const success = await this.startMacOSAudioCapture();
|
||||||
return { success };
|
return { success, error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting macOS audio capture:', error);
|
console.error('Error starting macOS audio capture:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@ -274,4 +297,4 @@ class ListenService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ListenService;
|
module.exports = ListenService;
|
@ -1,5 +1,30 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
const { ipcRenderer } = require('electron');
|
||||||
|
const createAecModule = require('../../../assets/aec.js');
|
||||||
|
|
||||||
|
let aecModPromise = null; // 한 번만 로드
|
||||||
|
let aecMod = null;
|
||||||
|
let aecPtr = 0; // Rust Aec* 1개만 재사용
|
||||||
|
|
||||||
|
/** WASM 모듈 가져오고 1회 초기화 */
|
||||||
|
async function getAec () {
|
||||||
|
if (aecModPromise) return aecModPromise; // 캐시
|
||||||
|
|
||||||
|
aecModPromise = createAecModule().then((M) => {
|
||||||
|
aecMod = M;
|
||||||
|
// C 심볼 → JS 래퍼 바인딩 (딱 1번)
|
||||||
|
M.newPtr = M.cwrap('AecNew', 'number',
|
||||||
|
['number','number','number','number']);
|
||||||
|
M.cancel = M.cwrap('AecCancelEcho', null,
|
||||||
|
['number','number','number','number','number']);
|
||||||
|
M.destroy = M.cwrap('AecDestroy', null, ['number']);
|
||||||
|
return M;
|
||||||
|
});
|
||||||
|
|
||||||
|
return aecModPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 바로 로드-실패 로그를 보기 위해
|
||||||
|
getAec().catch(console.error);
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Constants & Globals
|
// Constants & Globals
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -80,128 +105,49 @@ function arrayBufferToBase64(buffer) {
|
|||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------
|
/* ───────────────────────── JS ↔︎ WASM 헬퍼 ───────────────────────── */
|
||||||
// Complete SimpleAEC implementation (exact from renderer.js)
|
function int16PtrFromFloat32(mod, f32) {
|
||||||
// ---------------------------
|
const len = f32.length;
|
||||||
class SimpleAEC {
|
const bytes = len * 2;
|
||||||
constructor() {
|
const ptr = mod._malloc(bytes);
|
||||||
this.adaptiveFilter = new Float32Array(1024);
|
// HEAP16이 없으면 HEAPU8.buffer로 직접 래핑
|
||||||
this.mu = 0.2;
|
const heapBuf = (mod.HEAP16 ? mod.HEAP16.buffer : mod.HEAPU8.buffer);
|
||||||
this.echoDelay = 100;
|
const i16 = new Int16Array(heapBuf, ptr, len);
|
||||||
this.sampleRate = 24000;
|
for (let i = 0; i < len; ++i) {
|
||||||
this.delaySamples = Math.floor((this.echoDelay / 1000) * this.sampleRate);
|
const s = Math.max(-1, Math.min(1, f32[i]));
|
||||||
|
i16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
||||||
this.echoGain = 0.5;
|
}
|
||||||
this.noiseFloor = 0.01;
|
return { ptr, view: i16 };
|
||||||
|
}
|
||||||
// 🔧 Adaptive-gain parameters (User-tuned, very aggressive)
|
|
||||||
this.targetErr = 0.002;
|
function float32FromInt16View(i16) {
|
||||||
this.adaptRate = 0.1;
|
const out = new Float32Array(i16.length);
|
||||||
|
for (let i = 0; i < i16.length; ++i) out[i] = i16[i] / 32768;
|
||||||
console.log('🎯 AEC initialized (hyper-aggressive)');
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
process(micData, systemData) {
|
/* 필요하다면 종료 시 */
|
||||||
if (!systemData || systemData.length === 0) {
|
function disposeAec () {
|
||||||
return micData;
|
getAec().then(mod => { if (aecPtr) mod.destroy(aecPtr); });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < systemData.length; i++) {
|
function runAecSync (micF32, sysF32) {
|
||||||
if (systemData[i] > 0.98) systemData[i] = 0.98;
|
if (!aecMod || !aecPtr || !aecMod.HEAPU8) return micF32; // 아직 모듈 안 뜸 → 패스
|
||||||
else if (systemData[i] < -0.98) systemData[i] = -0.98;
|
|
||||||
|
const len = micF32.length;
|
||||||
systemData[i] = Math.tanh(systemData[i] * 4);
|
const mic = int16PtrFromFloat32(aecMod, micF32);
|
||||||
}
|
const echo = int16PtrFromFloat32(aecMod, sysF32);
|
||||||
|
const out = aecMod._malloc(len * 2);
|
||||||
let sum2 = 0;
|
|
||||||
for (let i = 0; i < systemData.length; i++) sum2 += systemData[i] * systemData[i];
|
aecMod.cancel(aecPtr, mic.ptr, echo.ptr, out, len);
|
||||||
const rms = Math.sqrt(sum2 / systemData.length);
|
|
||||||
const targetRms = 0.08; // 🔧 기준 RMS (기존 0.1)
|
const heapBuf = (aecMod.HEAP16 ? aecMod.HEAP16.buffer : aecMod.HEAPU8.buffer);
|
||||||
const scale = targetRms / (rms + 1e-6); // 1e-6: 0-division 방지
|
const outF32 = float32FromInt16View(new Int16Array(heapBuf, out, len));
|
||||||
|
|
||||||
const output = new Float32Array(micData.length);
|
aecMod._free(mic.ptr); aecMod._free(echo.ptr); aecMod._free(out);
|
||||||
|
return outF32;
|
||||||
const optimalDelay = this.findOptimalDelay(micData, systemData);
|
|
||||||
|
|
||||||
for (let i = 0; i < micData.length; i++) {
|
|
||||||
let echoEstimate = 0;
|
|
||||||
|
|
||||||
for (let d = -500; d <= 500; d += 100) {
|
|
||||||
const delayIndex = i - optimalDelay - d;
|
|
||||||
if (delayIndex >= 0 && delayIndex < systemData.length) {
|
|
||||||
const weight = Math.exp(-Math.abs(d) / 1000);
|
|
||||||
echoEstimate += systemData[delayIndex] * scale * this.echoGain * weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output[i] = micData[i] - echoEstimate * 0.9;
|
|
||||||
|
|
||||||
if (Math.abs(output[i]) < this.noiseFloor) {
|
|
||||||
output[i] *= 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isSimilarToSystem(output[i], systemData, i, optimalDelay)) {
|
|
||||||
output[i] *= 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
output[i] = Math.max(-1, Math.min(1, output[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let errSum = 0;
|
|
||||||
for (let i = 0; i < output.length; i++) errSum += output[i] * output[i];
|
|
||||||
const errRms = Math.sqrt(errSum / output.length);
|
|
||||||
|
|
||||||
const err = errRms - this.targetErr;
|
|
||||||
this.echoGain += this.adaptRate * err; // 비례 제어
|
|
||||||
this.echoGain = Math.max(0, Math.min(1, this.echoGain));
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
findOptimalDelay(micData, systemData) {
|
|
||||||
let maxCorr = 0;
|
|
||||||
let optimalDelay = this.delaySamples;
|
|
||||||
|
|
||||||
for (let delay = 0; delay < 5000 && delay < systemData.length; delay += 200) {
|
|
||||||
let corr = 0;
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(500, micData.length); i++) {
|
|
||||||
if (i + delay < systemData.length) {
|
|
||||||
corr += micData[i] * systemData[i + delay];
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
corr = Math.abs(corr / count);
|
|
||||||
if (corr > maxCorr) {
|
|
||||||
maxCorr = corr;
|
|
||||||
optimalDelay = delay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return optimalDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSimilarToSystem(sample, systemData, index, delay) {
|
|
||||||
const windowSize = 50;
|
|
||||||
let similarity = 0;
|
|
||||||
|
|
||||||
for (let i = -windowSize; i <= windowSize; i++) {
|
|
||||||
const sysIndex = index - delay + i;
|
|
||||||
if (sysIndex >= 0 && sysIndex < systemData.length) {
|
|
||||||
similarity += Math.abs(sample - systemData[sysIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return similarity / (2 * windowSize + 1) < 0.15;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let aecProcessor = new SimpleAEC();
|
|
||||||
|
|
||||||
// System audio data handler
|
// System audio data handler
|
||||||
ipcRenderer.on('system-audio-data', (event, { data }) => {
|
ipcRenderer.on('system-audio-data', (event, { data }) => {
|
||||||
@ -214,8 +160,6 @@ ipcRenderer.on('system-audio-data', (event, { data }) => {
|
|||||||
if (systemAudioBuffer.length > MAX_SYSTEM_BUFFER_SIZE) {
|
if (systemAudioBuffer.length > MAX_SYSTEM_BUFFER_SIZE) {
|
||||||
systemAudioBuffer = systemAudioBuffer.slice(-MAX_SYSTEM_BUFFER_SIZE);
|
systemAudioBuffer = systemAudioBuffer.slice(-MAX_SYSTEM_BUFFER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📥 Received system audio for AEC reference');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -305,39 +249,47 @@ setInterval(() => {
|
|||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Audio processing functions (exact from renderer.js)
|
// Audio processing functions (exact from renderer.js)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
function setupMicProcessing(micStream) {
|
async function setupMicProcessing(micStream) {
|
||||||
|
/* ── WASM 먼저 로드 ───────────────────────── */
|
||||||
|
const mod = await getAec();
|
||||||
|
if (!aecPtr) aecPtr = mod.newPtr(160, 1600, 24000, 1);
|
||||||
|
|
||||||
|
|
||||||
const micAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
const micAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||||
|
await micAudioContext.resume();
|
||||||
const micSource = micAudioContext.createMediaStreamSource(micStream);
|
const micSource = micAudioContext.createMediaStreamSource(micStream);
|
||||||
const micProcessor = micAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
const micProcessor = micAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
||||||
|
|
||||||
let audioBuffer = [];
|
let audioBuffer = [];
|
||||||
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
||||||
|
|
||||||
micProcessor.onaudioprocess = async e => {
|
micProcessor.onaudioprocess = (e) => {
|
||||||
const inputData = e.inputBuffer.getChannelData(0);
|
const inputData = e.inputBuffer.getChannelData(0);
|
||||||
audioBuffer.push(...inputData);
|
audioBuffer.push(...inputData);
|
||||||
|
console.log('🎤 micProcessor.onaudioprocess');
|
||||||
|
|
||||||
|
// samplesPerChunk(=2400) 만큼 모이면 전송
|
||||||
while (audioBuffer.length >= samplesPerChunk) {
|
while (audioBuffer.length >= samplesPerChunk) {
|
||||||
let chunk = audioBuffer.splice(0, samplesPerChunk);
|
let chunk = audioBuffer.splice(0, samplesPerChunk);
|
||||||
let processedChunk = new Float32Array(chunk);
|
let processedChunk = new Float32Array(chunk); // 기본값
|
||||||
|
|
||||||
// Check for system audio and apply AEC only if voice is active
|
// ───────────────── WASM AEC ─────────────────
|
||||||
if (aecProcessor && systemAudioBuffer.length > 0) {
|
if (systemAudioBuffer.length > 0) {
|
||||||
const latestSystemAudio = systemAudioBuffer[systemAudioBuffer.length - 1];
|
const latest = systemAudioBuffer[systemAudioBuffer.length - 1];
|
||||||
const systemFloat32 = base64ToFloat32Array(latestSystemAudio.data);
|
const sysF32 = base64ToFloat32Array(latest.data);
|
||||||
|
|
||||||
// Apply AEC only when system audio has active speech
|
// **음성 구간일 때만 런**
|
||||||
if (isVoiceActive(systemFloat32)) {
|
processedChunk = runAecSync(new Float32Array(chunk), sysF32);
|
||||||
processedChunk = aecProcessor.process(new Float32Array(chunk), systemFloat32);
|
console.log('🔊 Applied WASM-AEC (speex)');
|
||||||
console.log('🔊 Applied AEC because system audio is active');
|
} else {
|
||||||
}
|
console.log('🔊 No system audio for AEC reference');
|
||||||
}
|
}
|
||||||
|
|
||||||
const pcmData16 = convertFloat32ToInt16(processedChunk);
|
const pcm16 = convertFloat32ToInt16(processedChunk);
|
||||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
const b64 = arrayBufferToBase64(pcm16.buffer);
|
||||||
|
|
||||||
await ipcRenderer.invoke('send-audio-content', {
|
ipcRenderer.invoke('send-audio-content', {
|
||||||
data: base64Data,
|
data: b64,
|
||||||
mimeType: 'audio/pcm;rate=24000',
|
mimeType: 'audio/pcm;rate=24000',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -520,7 +472,19 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
|||||||
// Start macOS audio capture
|
// Start macOS audio capture
|
||||||
const audioResult = await ipcRenderer.invoke('start-macos-audio');
|
const audioResult = await ipcRenderer.invoke('start-macos-audio');
|
||||||
if (!audioResult.success) {
|
if (!audioResult.success) {
|
||||||
throw new Error('Failed to start macOS audio capture: ' + audioResult.error);
|
console.warn('[listenCapture] macOS audio start failed:', audioResult.error);
|
||||||
|
|
||||||
|
// 이미 실행 중 → stop 후 재시도
|
||||||
|
if (audioResult.error === 'already_running') {
|
||||||
|
await ipcRenderer.invoke('stop-macos-audio');
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
const retry = await ipcRenderer.invoke('start-macos-audio');
|
||||||
|
if (!retry.success) {
|
||||||
|
throw new Error('Retry failed: ' + retry.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to start macOS audio capture: ' + audioResult.error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize screen capture in main process
|
// Initialize screen capture in main process
|
||||||
@ -543,7 +507,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('macOS microphone capture started');
|
console.log('macOS microphone capture started');
|
||||||
const { context, processor } = setupMicProcessing(micMediaStream);
|
const { context, processor } = await setupMicProcessing(micMediaStream);
|
||||||
audioContext = context;
|
audioContext = context;
|
||||||
audioProcessor = processor;
|
audioProcessor = processor;
|
||||||
} catch (micErr) {
|
} catch (micErr) {
|
||||||
@ -616,7 +580,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
|||||||
video: false,
|
video: false,
|
||||||
});
|
});
|
||||||
console.log('Windows microphone capture started');
|
console.log('Windows microphone capture started');
|
||||||
const { context, processor } = setupMicProcessing(micMediaStream);
|
const { context, processor } = await setupMicProcessing(micMediaStream);
|
||||||
audioContext = context;
|
audioContext = context;
|
||||||
audioProcessor = processor;
|
audioProcessor = processor;
|
||||||
} catch (micErr) {
|
} catch (micErr) {
|
||||||
@ -719,6 +683,9 @@ function stopCapture() {
|
|||||||
// Exports & global registration
|
// Exports & global registration
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getAec, // 새로 만든 초기화 함수
|
||||||
|
runAecSync, // sync 버전
|
||||||
|
disposeAec, // 필요시 Rust 객체 파괴
|
||||||
startCapture,
|
startCapture,
|
||||||
stopCapture,
|
stopCapture,
|
||||||
captureManualScreenshot,
|
captureManualScreenshot,
|
||||||
|
@ -222,6 +222,8 @@ class SttService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTheirMessage = message => {
|
const handleTheirMessage = message => {
|
||||||
|
if (!message || typeof message !== 'object') return;
|
||||||
|
|
||||||
if (!this.modelInfo) {
|
if (!this.modelInfo) {
|
||||||
console.log('[SttService] Ignoring message - session already closed');
|
console.log('[SttService] Ignoring message - session already closed');
|
||||||
return;
|
return;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user