Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76d6fc2749 | ||
|
|
2f013b4751 | ||
|
|
66dce4415a | ||
|
|
6460349fc7 | ||
|
|
6926c27f20 | ||
|
|
cbf82f9317 | ||
|
|
bf9ec15912 | ||
|
|
01b8415a02 | ||
|
|
4edfaf4906 |
@@ -10,6 +10,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
|
||||
+33
-8
@@ -1,5 +1,7 @@
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
@@ -7,18 +9,37 @@ module.exports = {
|
||||
extraResource: ['./src/assets/SystemAudioDump'],
|
||||
name: 'Cheating Daddy',
|
||||
icon: 'src/assets/logo',
|
||||
// Fix executable permissions after packaging
|
||||
afterCopy: [
|
||||
(buildPath, electronVersion, platform, arch, callback) => {
|
||||
if (platform === 'darwin') {
|
||||
const systemAudioDump = path.join(buildPath, '..', 'Resources', 'SystemAudioDump');
|
||||
if (fs.existsSync(systemAudioDump)) {
|
||||
try {
|
||||
fs.chmodSync(systemAudioDump, 0o755);
|
||||
console.log('✓ Set executable permissions for SystemAudioDump');
|
||||
} catch (err) {
|
||||
console.error('✗ Failed to set permissions:', err.message);
|
||||
}
|
||||
} else {
|
||||
console.warn('SystemAudioDump not found at:', systemAudioDump);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
],
|
||||
// use `security find-identity -v -p codesigning` to find your identity
|
||||
// for macos signing
|
||||
// also fuck apple
|
||||
// Disabled for local builds - ad-hoc signing causes issues
|
||||
// osxSign: {
|
||||
// identity: '<paste your identity here>',
|
||||
// optionsForFile: (filePath) => {
|
||||
// return {
|
||||
// entitlements: 'entitlements.plist',
|
||||
// };
|
||||
// },
|
||||
// identity: '-', // ad-hoc signing (no Apple Developer account needed)
|
||||
// optionsForFile: (filePath) => {
|
||||
// return {
|
||||
// entitlements: 'entitlements.plist',
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
// notarize if off cuz i ran this for 6 hours and it still didnt finish
|
||||
// notarize is off - requires Apple Developer account
|
||||
// osxNotarize: {
|
||||
// appleId: 'your apple id',
|
||||
// appleIdPassword: 'app specific password',
|
||||
@@ -40,6 +61,10 @@ module.exports = {
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
platforms: ['darwin'],
|
||||
config: {
|
||||
name: 'CheatingDaddy',
|
||||
format: 'ULFO',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@reforged/maker-appimage',
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cheating-daddy",
|
||||
"productName": "cheating-daddy",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.9",
|
||||
"description": "cheating daddy",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
@@ -19,8 +19,8 @@
|
||||
"cheating daddy ai assistant for interviews"
|
||||
],
|
||||
"author": {
|
||||
"name": "sohzm",
|
||||
"email": "sohambharambe9@gmail.com"
|
||||
"name": "ShiftyX1",
|
||||
"email": "lead@pyserve.org"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -39,6 +39,7 @@
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
|
||||
"@electron-forge/plugin-fuses": "^7.11.1",
|
||||
"@electron/fuses": "^2.0.0",
|
||||
"@electron/osx-sign": "^2.3.0",
|
||||
"@reforged/maker-appimage": "^5.1.1",
|
||||
"electron": "^39.2.7"
|
||||
}
|
||||
|
||||
Generated
+17
@@ -48,6 +48,9 @@ importers:
|
||||
'@electron/fuses':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@electron/osx-sign':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@reforged/maker-appimage':
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
@@ -173,6 +176,11 @@ packages:
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@electron/osx-sign@2.3.0':
|
||||
resolution: {integrity: sha512-qh/BkWUHITP2W1grtCWn8Pm4EML3q1Rfo2tnd/KHo+f1le5gAYotBc6yEfK6X2VHyN21mPZ3CjvOiYOwjimBlA==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
'@electron/packager@18.4.4':
|
||||
resolution: {integrity: sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==}
|
||||
engines: {node: '>= 16.13.0'}
|
||||
@@ -2487,6 +2495,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@electron/osx-sign@2.3.0':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
isbinaryfile: 4.0.10
|
||||
plist: 3.1.0
|
||||
semver: 7.7.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@electron/packager@18.4.4':
|
||||
dependencies:
|
||||
'@electron/asar': 3.4.1
|
||||
|
||||
@@ -170,6 +170,25 @@ export class HelpView extends LitElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.open-logs-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.open-logs-btn:hover {
|
||||
background: var(--hover-background);
|
||||
}
|
||||
|
||||
.usage-steps {
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
@@ -442,9 +461,33 @@ export class HelpView extends LitElement {
|
||||
</div>
|
||||
<div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div>
|
||||
</div>
|
||||
|
||||
<div class="option-group">
|
||||
<div class="option-label">
|
||||
<span>Troubleshooting</span>
|
||||
</div>
|
||||
<div class="description" style="margin-bottom: 12px;">
|
||||
If you're experiencing issues with audio capture or other features, check the application logs for diagnostic information.
|
||||
</div>
|
||||
<button class="open-logs-btn" @click=${this.openLogsFolder}>
|
||||
📁 Open Logs Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async openLogsFolder() {
|
||||
try {
|
||||
const { ipcRenderer } = require('electron');
|
||||
const result = await ipcRenderer.invoke('open-logs-folder');
|
||||
if (!result.success) {
|
||||
console.error('Failed to open logs folder:', result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening logs folder:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('help-view', HelpView);
|
||||
|
||||
@@ -6,6 +6,7 @@ const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
||||
const { createWindow, updateGlobalShortcuts } = require('./utils/window');
|
||||
const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager');
|
||||
const { stopMacOSAudioCapture } = require('./utils/gemini');
|
||||
const { initLogger, closeLogger, getLogPath } = require('./utils/logger');
|
||||
const storage = require('./storage');
|
||||
|
||||
const geminiSessionRef = { current: null };
|
||||
@@ -24,6 +25,10 @@ function createMainWindow() {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Initialize file logger first
|
||||
const logPath = initLogger();
|
||||
console.log('App starting, log file:', logPath);
|
||||
|
||||
// Initialize storage (checks version, resets if needed)
|
||||
storage.initializeStorage();
|
||||
|
||||
@@ -31,10 +36,22 @@ app.whenReady().then(async () => {
|
||||
setupAIProviderIpcHandlers(geminiSessionRef);
|
||||
setupStorageIpcHandlers();
|
||||
setupGeneralIpcHandlers();
|
||||
|
||||
// Add handler to get log path from renderer
|
||||
ipcMain.handle('get-log-path', () => getLogPath());
|
||||
|
||||
// Add handler for renderer logs (so they go to the log file)
|
||||
ipcMain.on('renderer-log', (event, { level, message }) => {
|
||||
const prefix = '[RENDERER]';
|
||||
if (level === 'error') console.error(prefix, message);
|
||||
else if (level === 'warn') console.warn(prefix, message);
|
||||
else console.log(prefix, message);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
stopMacOSAudioCapture();
|
||||
closeLogger();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
@@ -42,6 +59,7 @@ app.on('window-all-closed', () => {
|
||||
|
||||
app.on('before-quit', () => {
|
||||
stopMacOSAudioCapture();
|
||||
closeLogger();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
@@ -284,6 +302,18 @@ function setupGeneralIpcHandlers() {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.handle('open-logs-folder', async () => {
|
||||
try {
|
||||
const logPath = getLogPath();
|
||||
const logsDir = require('path').dirname(logPath);
|
||||
await shell.openPath(logsDir);
|
||||
return { success: true, path: logsDir };
|
||||
} catch (error) {
|
||||
console.error('Error opening logs folder:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('quit-application', async event => {
|
||||
try {
|
||||
stopMacOSAudioCapture();
|
||||
|
||||
+59
-6
@@ -418,10 +418,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
// Kill any existing SystemAudioDump processes first
|
||||
await killExistingSystemAudioDump();
|
||||
|
||||
console.log('Starting macOS audio capture with SystemAudioDump...');
|
||||
console.log('=== Starting macOS audio capture ===');
|
||||
sendToRenderer('update-status', 'Starting audio capture...');
|
||||
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
let systemAudioPath;
|
||||
if (app.isPackaged) {
|
||||
@@ -430,7 +432,35 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
|
||||
}
|
||||
|
||||
console.log('SystemAudioDump path:', systemAudioPath);
|
||||
console.log('SystemAudioDump config:', {
|
||||
path: systemAudioPath,
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
exists: fs.existsSync(systemAudioPath),
|
||||
});
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(systemAudioPath)) {
|
||||
console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
|
||||
sendToRenderer('update-status', 'Error: Audio binary not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check and fix executable permissions
|
||||
try {
|
||||
fs.accessSync(systemAudioPath, fs.constants.X_OK);
|
||||
console.log('SystemAudioDump is executable');
|
||||
} catch (err) {
|
||||
console.warn('SystemAudioDump not executable, fixing permissions...');
|
||||
try {
|
||||
fs.chmodSync(systemAudioPath, 0o755);
|
||||
console.log('Fixed executable permissions');
|
||||
} catch (chmodErr) {
|
||||
console.error('Failed to fix permissions:', chmodErr);
|
||||
sendToRenderer('update-status', 'Error: Cannot execute audio binary');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@@ -439,10 +469,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
},
|
||||
};
|
||||
|
||||
console.log('Spawning SystemAudioDump...');
|
||||
systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
|
||||
|
||||
if (!systemAudioProc.pid) {
|
||||
console.error('Failed to start SystemAudioDump');
|
||||
console.error('FATAL: Failed to start SystemAudioDump - no PID');
|
||||
sendToRenderer('update-status', 'Error: Audio capture failed to start');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -455,8 +487,16 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
|
||||
|
||||
let audioBuffer = Buffer.alloc(0);
|
||||
let chunkCount = 0;
|
||||
let firstDataReceived = false;
|
||||
|
||||
systemAudioProc.stdout.on('data', data => {
|
||||
if (!firstDataReceived) {
|
||||
firstDataReceived = true;
|
||||
console.log('First audio data received! Size:', data.length);
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
|
||||
audioBuffer = Buffer.concat([audioBuffer, data]);
|
||||
|
||||
while (audioBuffer.length >= CHUNK_SIZE) {
|
||||
@@ -467,6 +507,11 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
const base64Data = monoChunk.toString('base64');
|
||||
sendAudioToGemini(base64Data, geminiSessionRef);
|
||||
|
||||
chunkCount++;
|
||||
if (chunkCount % 100 === 0) {
|
||||
console.log(`Audio: ${chunkCount} chunks processed`);
|
||||
}
|
||||
|
||||
if (process.env.DEBUG_AUDIO) {
|
||||
console.log(`Processed audio chunk: ${chunk.length} bytes`);
|
||||
saveDebugAudio(monoChunk, 'system_audio');
|
||||
@@ -480,16 +525,24 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
});
|
||||
|
||||
systemAudioProc.stderr.on('data', data => {
|
||||
console.error('SystemAudioDump stderr:', data.toString());
|
||||
const msg = data.toString();
|
||||
console.error('SystemAudioDump stderr:', msg);
|
||||
if (msg.toLowerCase().includes('error')) {
|
||||
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
|
||||
}
|
||||
});
|
||||
|
||||
systemAudioProc.on('close', code => {
|
||||
console.log('SystemAudioDump process closed with code:', code);
|
||||
console.log('SystemAudioDump closed with code:', code, 'chunks processed:', chunkCount);
|
||||
if (code !== 0 && code !== null) {
|
||||
sendToRenderer('update-status', `Audio stopped (exit: ${code})`);
|
||||
}
|
||||
systemAudioProc = null;
|
||||
});
|
||||
|
||||
systemAudioProc.on('error', err => {
|
||||
console.error('SystemAudioDump process error:', err);
|
||||
console.error('SystemAudioDump spawn error:', err.message);
|
||||
sendToRenderer('update-status', 'Audio error: ' + err.message);
|
||||
systemAudioProc = null;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { app } = require('electron');
|
||||
|
||||
let logFile = null;
|
||||
let logPath = null;
|
||||
|
||||
function getLogPath() {
|
||||
if (logPath) return logPath;
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
const logsDir = path.join(userDataPath, 'logs');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create log file with timestamp
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
logPath = path.join(logsDir, `app-${timestamp}.log`);
|
||||
|
||||
return logPath;
|
||||
}
|
||||
|
||||
function initLogger() {
|
||||
try {
|
||||
const filePath = getLogPath();
|
||||
logFile = fs.createWriteStream(filePath, { flags: 'a' });
|
||||
|
||||
const startMsg = `\n${'='.repeat(60)}\nApp started at ${new Date().toISOString()}\nPlatform: ${process.platform}, Arch: ${process.arch}\nElectron: ${process.versions.electron}, Node: ${process.versions.node}\nPackaged: ${app.isPackaged}\n${'='.repeat(60)}\n`;
|
||||
logFile.write(startMsg);
|
||||
|
||||
// Override console methods to also write to file
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
console.log = (...args) => {
|
||||
originalLog.apply(console, args);
|
||||
writeLog('INFO', args);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
originalError.apply(console, args);
|
||||
writeLog('ERROR', args);
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
originalWarn.apply(console, args);
|
||||
writeLog('WARN', args);
|
||||
};
|
||||
|
||||
console.log('Logger initialized, writing to:', filePath);
|
||||
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize logger:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(level, args) {
|
||||
if (!logFile) return;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = args.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ');
|
||||
|
||||
logFile.write(`[${timestamp}] [${level}] ${message}\n`);
|
||||
} catch (err) {
|
||||
// Silently fail - don't want logging errors to crash the app
|
||||
}
|
||||
}
|
||||
|
||||
function closeLogger() {
|
||||
if (logFile) {
|
||||
logFile.write(`\nApp closed at ${new Date().toISOString()}\n`);
|
||||
logFile.end();
|
||||
logFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initLogger,
|
||||
closeLogger,
|
||||
getLogPath,
|
||||
};
|
||||
+79
-9
@@ -281,6 +281,7 @@ async function sendImageMessage(base64Image, prompt) {
|
||||
let audioChunks = [];
|
||||
let lastAudioTime = 0;
|
||||
const SILENCE_THRESHOLD_MS = 1500; // 1.5 seconds of silence
|
||||
let silenceCheckTimer = null;
|
||||
|
||||
async function processAudioChunk(base64Audio, mimeType) {
|
||||
if (!openaiClient) {
|
||||
@@ -294,8 +295,20 @@ async function processAudioChunk(base64Audio, mimeType) {
|
||||
audioChunks.push(buffer);
|
||||
lastAudioTime = now;
|
||||
|
||||
// Check for silence (no new audio for SILENCE_THRESHOLD_MS)
|
||||
// This is a simple approach - in production you'd want proper VAD
|
||||
// Clear existing timer
|
||||
if (silenceCheckTimer) {
|
||||
clearTimeout(silenceCheckTimer);
|
||||
}
|
||||
|
||||
// Set timer to check for silence
|
||||
silenceCheckTimer = setTimeout(async () => {
|
||||
const silenceDuration = Date.now() - lastAudioTime;
|
||||
if (silenceDuration >= SILENCE_THRESHOLD_MS && audioChunks.length > 0) {
|
||||
console.log('Silence detected, flushing audio for transcription...');
|
||||
await flushAudioAndTranscribe();
|
||||
}
|
||||
}, SILENCE_THRESHOLD_MS);
|
||||
|
||||
return { success: true, buffering: true };
|
||||
}
|
||||
|
||||
@@ -441,9 +454,11 @@ async function startMacOSAudioCapture() {
|
||||
// Kill any existing SystemAudioDump processes first
|
||||
await killExistingSystemAudioDump();
|
||||
|
||||
console.log('Starting macOS audio capture with SystemAudioDump for OpenAI SDK...');
|
||||
console.log('=== Starting macOS audio capture (OpenAI SDK) ===');
|
||||
sendToRenderer('update-status', 'Starting audio capture...');
|
||||
|
||||
const { app } = require('electron');
|
||||
const fs = require('fs');
|
||||
|
||||
let systemAudioPath;
|
||||
if (app.isPackaged) {
|
||||
@@ -452,7 +467,35 @@ async function startMacOSAudioCapture() {
|
||||
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
|
||||
}
|
||||
|
||||
console.log('SystemAudioDump path:', systemAudioPath);
|
||||
console.log('SystemAudioDump config:', {
|
||||
path: systemAudioPath,
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
exists: fs.existsSync(systemAudioPath),
|
||||
});
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(systemAudioPath)) {
|
||||
console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
|
||||
sendToRenderer('update-status', 'Error: Audio binary not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check and fix executable permissions
|
||||
try {
|
||||
fs.accessSync(systemAudioPath, fs.constants.X_OK);
|
||||
console.log('SystemAudioDump is executable');
|
||||
} catch (err) {
|
||||
console.warn('SystemAudioDump not executable, fixing permissions...');
|
||||
try {
|
||||
fs.chmodSync(systemAudioPath, 0o755);
|
||||
console.log('Fixed executable permissions');
|
||||
} catch (chmodErr) {
|
||||
console.error('Failed to fix permissions:', chmodErr);
|
||||
sendToRenderer('update-status', 'Error: Cannot execute audio binary');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@@ -461,10 +504,12 @@ async function startMacOSAudioCapture() {
|
||||
},
|
||||
};
|
||||
|
||||
console.log('Spawning SystemAudioDump...');
|
||||
systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
|
||||
|
||||
if (!systemAudioProc.pid) {
|
||||
console.error('Failed to start SystemAudioDump');
|
||||
console.error('FATAL: Failed to start SystemAudioDump - no PID');
|
||||
sendToRenderer('update-status', 'Error: Audio capture failed to start');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -476,8 +521,16 @@ async function startMacOSAudioCapture() {
|
||||
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
|
||||
|
||||
let tempBuffer = Buffer.alloc(0);
|
||||
let chunkCount = 0;
|
||||
let firstDataReceived = false;
|
||||
|
||||
systemAudioProc.stdout.on('data', data => {
|
||||
if (!firstDataReceived) {
|
||||
firstDataReceived = true;
|
||||
console.log('First audio data received! Size:', data.length);
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
|
||||
tempBuffer = Buffer.concat([tempBuffer, data]);
|
||||
|
||||
while (tempBuffer.length >= CHUNK_SIZE) {
|
||||
@@ -489,6 +542,11 @@ async function startMacOSAudioCapture() {
|
||||
|
||||
// Add to audio buffer for transcription
|
||||
audioBuffer = Buffer.concat([audioBuffer, monoChunk]);
|
||||
|
||||
chunkCount++;
|
||||
if (chunkCount % 100 === 0) {
|
||||
console.log(`Audio: ${chunkCount} chunks processed, buffer size: ${audioBuffer.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit buffer size (max 30 seconds of audio)
|
||||
@@ -499,21 +557,33 @@ async function startMacOSAudioCapture() {
|
||||
});
|
||||
|
||||
systemAudioProc.stderr.on('data', data => {
|
||||
console.error('SystemAudioDump stderr:', data.toString());
|
||||
const msg = data.toString();
|
||||
console.error('SystemAudioDump stderr:', msg);
|
||||
if (msg.toLowerCase().includes('error')) {
|
||||
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
|
||||
}
|
||||
});
|
||||
|
||||
systemAudioProc.on('close', code => {
|
||||
console.log('SystemAudioDump process closed with code:', code);
|
||||
systemAudioProc.on('close', (code, signal) => {
|
||||
console.log('SystemAudioDump closed:', { code, signal, chunksProcessed: chunkCount, tempBufferSize: tempBuffer.length });
|
||||
if (code !== 0 && code !== null) {
|
||||
sendToRenderer('update-status', `Audio stopped (exit: ${code}, signal: ${signal})`);
|
||||
}
|
||||
systemAudioProc = null;
|
||||
stopTranscriptionTimer();
|
||||
});
|
||||
|
||||
systemAudioProc.on('error', err => {
|
||||
console.error('SystemAudioDump process error:', err);
|
||||
console.error('SystemAudioDump spawn error:', err.message, err.stack);
|
||||
sendToRenderer('update-status', 'Audio error: ' + err.message);
|
||||
systemAudioProc = null;
|
||||
stopTranscriptionTimer();
|
||||
});
|
||||
|
||||
systemAudioProc.on('exit', (code, signal) => {
|
||||
console.log('SystemAudioDump exit event:', { code, signal });
|
||||
});
|
||||
|
||||
// Start periodic transcription
|
||||
startTranscriptionTimer();
|
||||
|
||||
|
||||
@@ -18,6 +18,23 @@ let currentImageQuality = 'medium'; // Store current image quality for manual sc
|
||||
|
||||
const isLinux = process.platform === 'linux';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Send logs to main process for file logging
|
||||
function logToMain(level, ...args) {
|
||||
const message = args.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try { return JSON.stringify(arg); } catch { return String(arg); }
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ');
|
||||
ipcRenderer.send('renderer-log', { level, message });
|
||||
|
||||
// Also log to console
|
||||
if (level === 'error') console.error(...args);
|
||||
else if (level === 'warn') console.warn(...args);
|
||||
else console.log(...args);
|
||||
}
|
||||
|
||||
// ============ STORAGE API ============
|
||||
// Wrapper for IPC-based storage access
|
||||
@@ -294,6 +311,20 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
});
|
||||
|
||||
console.log('Windows capture started with loopback audio');
|
||||
const audioTracks = mediaStream.getAudioTracks();
|
||||
const videoTracks = mediaStream.getVideoTracks();
|
||||
|
||||
logToMain('info', 'Windows capture result:', {
|
||||
hasVideo: videoTracks.length > 0,
|
||||
hasAudio: audioTracks.length > 0,
|
||||
audioTrackInfo: audioTracks.map(t => ({
|
||||
label: t.label,
|
||||
enabled: t.enabled,
|
||||
muted: t.muted,
|
||||
readyState: t.readyState,
|
||||
settings: t.getSettings()
|
||||
})),
|
||||
});
|
||||
|
||||
// Setup audio processing for Windows loopback audio only
|
||||
setupWindowsLoopbackProcessing();
|
||||
|
||||
+8
-53
@@ -33,59 +33,14 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
||||
});
|
||||
|
||||
const { session, desktopCapturer } = require('electron');
|
||||
|
||||
// Setup display media request handler for screen capture
|
||||
// On macOS, use system picker for better UX
|
||||
if (process.platform === 'darwin') {
|
||||
session.defaultSession.setDisplayMediaRequestHandler(
|
||||
async (request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: { width: 0, height: 0 } // Skip thumbnail generation for speed
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
console.error('No screen sources available');
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// On macOS, directly use the first screen (system already granted permission)
|
||||
console.log('Screen capture source:', sources[0].name);
|
||||
callback({ video: sources[0], audio: 'loopback' });
|
||||
} catch (error) {
|
||||
console.error('Error getting screen sources:', error);
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
{ useSystemPicker: false } // Disable system picker, use our source directly
|
||||
);
|
||||
} else {
|
||||
// On other platforms, use the system picker
|
||||
session.defaultSession.setDisplayMediaRequestHandler(
|
||||
async (request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: { width: 0, height: 0 }
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
console.error('No screen sources available');
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
callback({ video: sources[0], audio: 'loopback' });
|
||||
} catch (error) {
|
||||
console.error('Error getting screen sources:', error);
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
{ useSystemPicker: true }
|
||||
);
|
||||
}
|
||||
session.defaultSession.setDisplayMediaRequestHandler(
|
||||
(request, callback) => {
|
||||
desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
|
||||
callback({ video: sources[0], audio: 'loopback' });
|
||||
});
|
||||
},
|
||||
{ useSystemPicker: true }
|
||||
);
|
||||
|
||||
mainWindow.setResizable(false);
|
||||
mainWindow.setContentProtection(true);
|
||||
|
||||
Reference in New Issue
Block a user