From 8f1b1f69de0b7eb9d16299c97f18de959c696ba7 Mon Sep 17 00:00:00 2001 From: Dhriti Date: Fri, 17 Apr 2026 22:01:22 +0530 Subject: [PATCH 1/6] added --- .kwin_debug.js | 5 + README.md | 5 +- src/main/index.js | 4 +- src/main/ipc-handlers.js | 3 + src/main/kde-manager.js | 318 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 .kwin_debug.js create mode 100644 src/main/kde-manager.js diff --git a/.kwin_debug.js b/.kwin_debug.js new file mode 100644 index 0000000..e51521c --- /dev/null +++ b/.kwin_debug.js @@ -0,0 +1,5 @@ +var windows = workspace.windowList(); +for (var i = 0; i < windows.length; i++) { + var w = windows[i]; + console.log("KWIN_DEBUG: class=" + w.resourceClass + " | name=" + w.resourceName + " | caption=" + w.caption + " | appId=" + (w.desktopFileName || "N/A")); +} diff --git a/README.md b/README.md index 02c1eea..6397879 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Imposter provides a discreet, always-on-top AI layer designed for maximum invisibility. -* **Stealth Window**: Hardware-level DRM protection makes the AI invisible to screen-sharing, screenshots, and meeting platforms (Zoom, Teams). +* **Stealth Window**: Hardware-level DRM protection (Windows/macOS) and KWin Scripting (Linux KDE) makes the AI invisible to screen-sharing, screenshots, and meeting platforms (Zoom, Teams). * **Voice Transcription**: Real-time system audio capture streamed to AssemblyAI for live results. * **Screen OCR**: Local Tesseract.js extraction from any region-snip directly into your AI prompt. * **High-Stakes Personas**: 12 context-aware identities tailored for specific assessment scenarios. @@ -39,7 +39,8 @@ Imposter provides a discreet, always-on-top AI layer designed for maximum invisi Built on a specialized architecture, Imposter remains non-intrusive and computationally isolated until triggered. -* **Hardware DRM**: Blocks all standard capture APIs. +* **Hardware DRM**: Blocks all standard capture APIs on Windows and macOS. +* **KDE Plasma Stealth**: Native compositor integration via KWin scripting for true invisibility on Linux (KDE 6.6+). * **Disguised UI**: Instantly transform into a standard utility identity to stay under the radar. * **100% Local**: Zero telemetry. Prompts, conversations, and keys never leave your machine. diff --git a/src/main/index.js b/src/main/index.js index 2151e71..e7e5885 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -5,6 +5,7 @@ require('dotenv').config(); const { createMainWindow, getMainWindow } = require('./window-manager'); const { registerShortcuts, unregisterShortcuts } = require('./shortcuts'); const { registerIpcHandlers } = require('./ipc-handlers'); +const kdeManager = require('./kde-manager'); // ── Process-Level Crash Guards ────────────────────────────────────────────── process.on('uncaughtException', (error) => { @@ -95,8 +96,9 @@ if (!gotTheLock) { app.on('will-quit', () => { try { unregisterShortcuts(); + kdeManager.cleanupKdeRules(); } catch (err) { - console.error('Shortcut cleanup error:', err); + console.error('Cleanup error:', err); } }); diff --git a/src/main/ipc-handlers.js b/src/main/ipc-handlers.js index 567c8bc..bc81975 100644 --- a/src/main/ipc-handlers.js +++ b/src/main/ipc-handlers.js @@ -2,6 +2,7 @@ const { ipcMain, desktopCapturer, nativeImage, app, net } = require('electron'); const Tesseract = require('tesseract.js'); const { getMainWindow, createIslandWindow, closeIslandWindow, closeSnipperWindow } = require('./window-manager'); const { startTranscription, stopTranscription, sendAudioChunk, testConnection } = require('./transcription'); +const kdeManager = require('./kde-manager'); let handlersRegistered = false; @@ -114,11 +115,13 @@ function registerIpcHandlers() { mainWindow.setAlwaysOnTop(false); mainWindow.setResizable(true); mainWindow.setContentProtection(false); + kdeManager.setKdeStealth(false); } else { mainWindow.setSkipTaskbar(true); mainWindow.setAlwaysOnTop(true, 'screen-saver'); mainWindow.setResizable(false); mainWindow.setContentProtection(true); + kdeManager.setKdeStealth(true); } } catch (err) { console.error('[IPC] set-app-mode error:', err); diff --git a/src/main/kde-manager.js b/src/main/kde-manager.js new file mode 100644 index 0000000..954db97 --- /dev/null +++ b/src/main/kde-manager.js @@ -0,0 +1,318 @@ +// ── KDE Plasma Stealth Manager ────────────────────────────────────────────── +// Uses KWin Scripting API to set ExcludeFromCapture on KDE Plasma 6.6+. +// The kwinrulesrc approach does NOT work because excludeFromCapture is not +// exposed as a rule property in KDE 6.6.x. Instead, we load a KWin script +// that sets the property directly on matching windows via the compositor. +// This module is a silent no-op on non-KDE systems. + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync } = require('child_process'); + +const WM_CLASS = 'imposter'; // Must match the wmClass of our Electron windows +const SCRIPT_NAME = 'imposter_stealth'; + +// ── KWin Script Templates ─────────────────────────────────────────────────── + +/** + * Generates a KWin script that sets excludeFromCapture on matching windows. + * The script also hooks windowAdded so new windows (Island, Snipper) are caught. + */ +function generateStealthScript(enable) { + const value = enable ? 'true' : 'false'; + return ` +// Imposter Stealth Script — Auto-generated +// Sets excludeFromCapture on all windows with resourceClass "${WM_CLASS}" +(function() { + function applyToWindow(w) { + if (w.resourceClass === "${WM_CLASS}") { + w.excludeFromCapture = ${value}; + } + } + + // Apply to all existing windows + var windows = workspace.windowList(); + for (var i = 0; i < windows.length; i++) { + applyToWindow(windows[i]); + } + + ${enable ? ` + // Hook new windows so Island/Snipper windows are also caught + workspace.windowAdded.connect(function(w) { + applyToWindow(w); + }); + ` : ''} +})(); +`; +} + +/** + * Returns the path to store temporary KWin scripts. + */ +function getScriptDir() { + const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); + const dir = path.join(configHome, 'imposter'); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return dir; +} + +/** + * Returns the path for the stealth KWin script file. + */ +function getScriptPath() { + return path.join(getScriptDir(), 'stealth.js'); +} + +// ── KDE Detection ─────────────────────────────────────────────────────────── + +/** + * Detects if the current desktop environment is KDE Plasma. + * Returns an object with { isKde, version, supported }. + */ +function detectKdePlasma() { + if (process.platform !== 'linux') { + return { isKde: false, version: null, supported: false }; + } + + const desktop = (process.env.XDG_CURRENT_DESKTOP || '').toLowerCase(); + const sessionDesktop = (process.env.XDG_SESSION_DESKTOP || '').toLowerCase(); + const kdeVersion = process.env.KDE_SESSION_VERSION || ''; + + const isKde = desktop.includes('kde') || sessionDesktop.includes('kde') || + desktop.includes('plasma') || sessionDesktop.includes('plasma'); + + if (!isKde) { + return { isKde: false, version: null, supported: false }; + } + + // Try to get precise Plasma version via plasmashell + let plasmaVersion = null; + try { + const output = execSync('plasmashell --version 2>/dev/null', { + encoding: 'utf-8', + timeout: 3000 + }).trim(); + const match = output.match(/(\d+\.\d+(?:\.\d+)?)/); + if (match) { + plasmaVersion = match[1]; + } + } catch (_) { + if (kdeVersion) { + plasmaVersion = kdeVersion; + } + } + + // ExcludeFromCapture requires KDE Plasma 6.6+ + let supported = false; + if (plasmaVersion) { + const parts = plasmaVersion.split('.').map(Number); + const major = parts[0] || 0; + const minor = parts[1] || 0; + supported = (major > 6) || (major === 6 && minor >= 6); + } + + return { isKde: true, version: plasmaVersion, supported }; +} + +// ── KWin Script Loading ───────────────────────────────────────────────────── + +/** + * Finds which qdbus command is available (qdbus6 for KDE 6, qdbus for KDE 5). + * Returns the command name or null. + */ +function findQdbusCommand() { + for (const cmd of ['qdbus6', 'qdbus']) { + try { + execSync(`which ${cmd} 2>/dev/null`, { encoding: 'utf-8', timeout: 2000 }); + return cmd; + } catch (_) { /* not found */ } + } + return null; +} + +/** + * Unloads the imposter stealth script if it's currently loaded. + */ +function unloadStealthScript(qdbus) { + if (!qdbus) return; + + try { + const loaded = execSync( + `${qdbus} org.kde.KWin /Scripting org.kde.kwin.Scripting.isScriptLoaded "${SCRIPT_NAME}" 2>/dev/null`, + { encoding: 'utf-8', timeout: 5000 } + ).trim(); + + if (loaded === 'true') { + execSync( + `${qdbus} org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript "${SCRIPT_NAME}" 2>/dev/null`, + { encoding: 'utf-8', timeout: 5000 } + ); + console.log('[KDE] Unloaded previous stealth script'); + } + } catch (err) { + // Script may not have been loaded — that's fine + console.log('[KDE] No previous stealth script to unload'); + } +} + +/** + * Loads and runs a KWin script. + * @param {string} scriptPath - Path to the .js script file + * @param {string} qdbus - The qdbus command to use + * @returns {boolean} true if successful + */ +function loadAndRunScript(scriptPath, qdbus) { + try { + // Load the script + const scriptIdOutput = execSync( + `${qdbus} org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript "${scriptPath}" "${SCRIPT_NAME}" 2>&1`, + { encoding: 'utf-8', timeout: 5000 } + ).trim(); + + const scriptId = parseInt(scriptIdOutput, 10); + if (isNaN(scriptId)) { + console.error('[KDE] Failed to load script, got:', scriptIdOutput); + return false; + } + + console.log(`[KDE] Script loaded with ID: ${scriptId}`); + + // Run the script + execSync( + `${qdbus} org.kde.KWin /Scripting/Script${scriptId} org.kde.kwin.Script.run 2>&1`, + { encoding: 'utf-8', timeout: 5000 } + ); + + console.log('[KDE] Script executed successfully'); + return true; + } catch (err) { + console.error('[KDE] Script load/run failed:', err.message); + return false; + } +} + +// ── Cache ─────────────────────────────────────────────────────────────────── +let _kdeDetection = null; +let _qdbusCmd = undefined; // undefined = not checked yet, null = not found + +function getKdeDetection() { + if (_kdeDetection === null) { + _kdeDetection = detectKdePlasma(); + + if (_kdeDetection.isKde && !_kdeDetection.supported) { + console.warn( + `[KDE] Detected KDE Plasma ${_kdeDetection.version || 'unknown version'}. ` + + `ExcludeFromCapture requires Plasma 6.6+. ` + + `Stealth mode will use Electron defaults only — screen capture hiding will NOT work on this KDE version.` + ); + } else if (_kdeDetection.isKde && _kdeDetection.supported) { + console.log(`[KDE] Detected KDE Plasma ${_kdeDetection.version} — ExcludeFromCapture supported`); + } + } + return _kdeDetection; +} + +function getQdbus() { + if (_qdbusCmd === undefined) { + _qdbusCmd = findQdbusCommand(); + if (_qdbusCmd) { + console.log(`[KDE] Using D-Bus tool: ${_qdbusCmd}`); + } else { + console.warn('[KDE] Neither qdbus6 nor qdbus found — KWin scripting unavailable'); + } + } + return _qdbusCmd; +} + +// ── Public API ────────────────────────────────────────────────────────────── + +/** + * Returns true if the current desktop is KDE Plasma (any version). + */ +function isKdePlasma() { + return getKdeDetection().isKde; +} + +/** + * Enables or disables KDE stealth via KWin Scripting API. + * Silent no-op on non-KDE or unsupported KDE versions. + * + * @param {boolean} enable - true to hide from capture, false to show + */ +function setKdeStealth(enable) { + const detection = getKdeDetection(); + + if (!detection.isKde) return; + + if (!detection.supported) { + // Warning already logged during detection + return; + } + + const qdbus = getQdbus(); + if (!qdbus) return; + + try { + // Always unload any previous instance of our script + unloadStealthScript(qdbus); + + // Write the script to disk + const scriptPath = getScriptPath(); + const scriptContent = generateStealthScript(enable); + fs.writeFileSync(scriptPath, scriptContent, 'utf-8'); + + // Load and run + const success = loadAndRunScript(scriptPath, qdbus); + + if (success) { + console.log(`[KDE] Stealth mode ${enable ? 'ENABLED' : 'DISABLED'} — ExcludeFromCapture = ${enable}`); + } else { + console.error('[KDE] Failed to apply stealth mode'); + } + } catch (err) { + console.error('[KDE] Failed to set stealth mode:', err.message); + } +} + +/** + * Cleanup on app exit. Disables excludeFromCapture on our windows + * and unloads the KWin script. + * Called from app 'will-quit' handler. + */ +function cleanupKdeRules() { + const detection = getKdeDetection(); + + if (!detection.isKde || !detection.supported) return; + + const qdbus = getQdbus(); + if (!qdbus) return; + + try { + // Unload the persistent script so the windowAdded hook stops + unloadStealthScript(qdbus); + + // Run a one-shot script to disable excludeFromCapture on existing windows + const scriptPath = getScriptPath(); + const disableScript = generateStealthScript(false); + fs.writeFileSync(scriptPath, disableScript, 'utf-8'); + loadAndRunScript(scriptPath, qdbus); + + // Clean up the script file + try { + if (fs.existsSync(scriptPath)) fs.unlinkSync(scriptPath); + } catch (_) { /* best effort */ } + + console.log('[KDE] Stealth cleanup complete'); + } catch (err) { + console.error('[KDE] Cleanup error:', err.message); + } +} + +module.exports = { + isKdePlasma, + setKdeStealth, + cleanupKdeRules +}; From 8c69947ec3002db22499087b0ef6e6bc19c21d4c Mon Sep 17 00:00:00 2001 From: Dhriti Date: Fri, 17 Apr 2026 22:52:34 +0530 Subject: [PATCH 2/6] added --- src/main/ipc-handlers.js | 5 ++ src/main/preload.js | 1 + src/main/transcription.js | 42 +++++++++++++ src/renderer/js/assembly-service.js | 92 ++++++++++++++++------------- 4 files changed, 98 insertions(+), 42 deletions(-) diff --git a/src/main/ipc-handlers.js b/src/main/ipc-handlers.js index bc81975..1120f15 100644 --- a/src/main/ipc-handlers.js +++ b/src/main/ipc-handlers.js @@ -42,6 +42,11 @@ function registerIpcHandlers() { } }); + ipcMain.handle('is-native-linux-audio', () => { + // Use native pipewire record on KDE/Linux instead of Chromium WebRTC + return kdeManager.isKdePlasma(); + }); + ipcMain.handle('start-transcription', async (event, apiKeyFromRenderer) => { try { const apiKey = apiKeyFromRenderer || process.env.ASSEMBLY_AI_API_KEY || process.env.ASSEMBLY_API; diff --git a/src/main/preload.js b/src/main/preload.js index 7551e0c..54a578b 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -20,6 +20,7 @@ contextBridge.exposeInMainWorld('electronAPI', { onAiResponse: (callback) => ipcRenderer.on('ai-response', (event, value) => callback(value)), // Voice & Transcription + isNativeLinuxAudio: () => ipcRenderer.invoke('is-native-linux-audio'), getDesktopSourceId: () => ipcRenderer.invoke('get-desktop-source-id'), startTranscription: (apiKey) => ipcRenderer.invoke('start-transcription', apiKey), stopTranscription: () => ipcRenderer.send('stop-transcription'), diff --git a/src/main/transcription.js b/src/main/transcription.js index 00a8037..9846b82 100644 --- a/src/main/transcription.js +++ b/src/main/transcription.js @@ -1,8 +1,11 @@ const { WebSocket } = require('ws'); const { getMainWindow, getIslandWindow } = require('./window-manager'); +const { spawn } = require('child_process'); +const kdeManager = require('./kde-manager'); let assemblySocket = null; let isConnecting = false; +let nativeAudioProcess = null; const SAMPLE_RATE = 16000; function safeSendStatus(status, error) { @@ -42,6 +45,36 @@ function startTranscription(apiKey) { clearTimeout(connectionTimeout); isConnecting = false; safeSendStatus('connected'); + + // If we are on KDE/Linux, start the native PipeWire capture now + if (kdeManager.isKdePlasma()) { + console.log('[TRANSCRIPTION] Starting native PipeWire audio capture (pw-record)'); + try { + nativeAudioProcess = spawn('pw-record', [ + '--rate=16000', + '--channels=1', + '--format=s16le', + '-' // stdout + ]); + + nativeAudioProcess.stdout.on('data', (data) => { + if (assemblySocket && assemblySocket.readyState === WebSocket.OPEN) { + assemblySocket.send(data); + } + }); + + nativeAudioProcess.on('error', (err) => { + console.error('[TRANSCRIPTION] pw-record error:', err.message); + }); + + nativeAudioProcess.on('close', (code) => { + console.log(`[TRANSCRIPTION] pw-record exited with code ${code}`); + nativeAudioProcess = null; + }); + } catch (err) { + console.error('[TRANSCRIPTION] Failed to spawn pw-record:', err); + } + } }); assemblySocket.on('message', (message) => { @@ -89,6 +122,15 @@ function startTranscription(apiKey) { function stopTranscription() { isConnecting = false; + + // Kill native audio process if it exists + if (nativeAudioProcess) { + try { + nativeAudioProcess.kill('SIGTERM'); + } catch (_) {} + nativeAudioProcess = null; + } + if (assemblySocket) { try { if (assemblySocket.readyState === WebSocket.OPEN) { diff --git a/src/renderer/js/assembly-service.js b/src/renderer/js/assembly-service.js index 6435ae1..9d0fd96 100644 --- a/src/renderer/js/assembly-service.js +++ b/src/renderer/js/assembly-service.js @@ -14,52 +14,60 @@ export const AssemblyService = { // Clean up any previous session first this.stop(); - stream = await navigator.mediaDevices.getDisplayMedia({ - audio: true, - video: { width: 1, height: 1, frameRate: 1 } - }); - - stream.getVideoTracks().forEach(track => track.stop()); - - const audioTracks = stream.getAudioTracks(); - if (audioTracks.length === 0) { - throw new Error('No audio track found. Ensure Share Audio was selected.'); - } - - audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 }); - - if (audioContext.state === 'suspended') { - await audioContext.resume(); - } - - await audioContext.audioWorklet.addModule('js/pcm-worklet.js'); - - const mediaStream = new MediaStream(audioTracks); - sourceNode = audioContext.createMediaStreamSource(mediaStream); + const isNativeLinux = await window.electronAPI.isNativeLinuxAudio(); + + if (isNativeLinux) { + console.log('[ASSEMBLY] Using native Linux audio capture (bypassing WebRTC)'); + const started = await window.electronAPI.startTranscription(apiKey); + if (!started) throw new Error('Failed to start native transcription on backend'); + } else { + stream = await navigator.mediaDevices.getDisplayMedia({ + audio: true, + video: { width: 1, height: 1, frameRate: 1 } + }); + + stream.getVideoTracks().forEach(track => track.stop()); + + const audioTracks = stream.getAudioTracks(); + if (audioTracks.length === 0) { + throw new Error('No audio track found. Ensure Share Audio was selected.'); + } - const started = await window.electronAPI.startTranscription(apiKey); - if (!started) throw new Error('Failed to start transcription on backend'); + audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 }); + + if (audioContext.state === 'suspended') { + await audioContext.resume(); + } - workletNode = new AudioWorkletNode(audioContext, 'pcm-processor'); - - workletNode.port.onmessage = (event) => { - try { - const buffer = event.data; - if (!buffer) return; - const uint8Array = new Uint8Array(buffer); - let binary = ''; - const len = uint8Array.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode(uint8Array[i]); + await audioContext.audioWorklet.addModule('js/pcm-worklet.js'); + + const mediaStream = new MediaStream(audioTracks); + sourceNode = audioContext.createMediaStreamSource(mediaStream); + + const started = await window.electronAPI.startTranscription(apiKey); + if (!started) throw new Error('Failed to start transcription on backend'); + + workletNode = new AudioWorkletNode(audioContext, 'pcm-processor'); + + workletNode.port.onmessage = (event) => { + try { + const buffer = event.data; + if (!buffer) return; + const uint8Array = new Uint8Array(buffer); + let binary = ''; + const len = uint8Array.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(uint8Array[i]); + } + window.electronAPI.sendAudioChunk(btoa(binary)); + } catch (err) { + // Silent — audio processing is high-frequency } - window.electronAPI.sendAudioChunk(btoa(binary)); - } catch (err) { - // Silent — audio processing is high-frequency - } - }; + }; - sourceNode.connect(workletNode); - workletNode.connect(audioContext.destination); + sourceNode.connect(workletNode); + workletNode.connect(audioContext.destination); + } window.electronAPI.onTranscriptionData((data) => { try { From ef21cc11377964f28b24d1d184127aab5daa1297 Mon Sep 17 00:00:00 2001 From: Dhriti Date: Fri, 17 Apr 2026 23:38:22 +0530 Subject: [PATCH 3/6] feat: full kde system implemented --- src/main/kde-manager.js | 2 ++ src/main/transcription.js | 15 ++++++++++++--- src/renderer/js/app.js | 10 ++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/kde-manager.js b/src/main/kde-manager.js index 954db97..c4588a5 100644 --- a/src/main/kde-manager.js +++ b/src/main/kde-manager.js @@ -28,6 +28,8 @@ function generateStealthScript(enable) { function applyToWindow(w) { if (w.resourceClass === "${WM_CLASS}") { w.excludeFromCapture = ${value}; + w.skipTaskbar = ${value}; + w.skipPager = ${value}; } } diff --git a/src/main/transcription.js b/src/main/transcription.js index 9846b82..51edcd5 100644 --- a/src/main/transcription.js +++ b/src/main/transcription.js @@ -53,13 +53,22 @@ function startTranscription(apiKey) { nativeAudioProcess = spawn('pw-record', [ '--rate=16000', '--channels=1', - '--format=s16le', + '--format=s16', '-' // stdout ]); + let audioBuffer = Buffer.alloc(0); + // 16000Hz * 1 channel * 2 bytes = 32000 bytes/sec. 100ms = 3200 bytes. + const MIN_CHUNK_SIZE = 3200; + nativeAudioProcess.stdout.on('data', (data) => { - if (assemblySocket && assemblySocket.readyState === WebSocket.OPEN) { - assemblySocket.send(data); + audioBuffer = Buffer.concat([audioBuffer, data]); + + if (audioBuffer.length >= MIN_CHUNK_SIZE) { + if (assemblySocket && assemblySocket.readyState === WebSocket.OPEN) { + assemblySocket.send(audioBuffer); + } + audioBuffer = Buffer.alloc(0); } }); diff --git a/src/renderer/js/app.js b/src/renderer/js/app.js index ef39a64..3157568 100644 --- a/src/renderer/js/app.js +++ b/src/renderer/js/app.js @@ -615,8 +615,14 @@ function setupEventListeners() { if (newGeminiModelSelect) { newGeminiModelSelect.setOptions([]); } - } else if (!modelId && provider === 'gemini') { - alert('Please enter a valid API key and select a model from the list.'); + } else { + if (!name) { + alert('Please enter a Label/Name for your model.'); + } else if (!modelId && provider === 'gemini') { + alert('Please verify and select a Gemini model from the dropdown.'); + } else if (!modelId) { + alert('Please enter a valid Model ID.'); + } } } catch (err) { console.error('[APP] Add model error:', err); From 631abe2f157b167e03502161c26a4e4e6e7e78ec Mon Sep 17 00:00:00 2001 From: Dhriti Date: Fri, 17 Apr 2026 23:41:08 +0530 Subject: [PATCH 4/6] doc: readme updated --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6397879..db5e653 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Imposter provides a discreet, always-on-top AI layer designed for maximum invisibility. * **Stealth Window**: Hardware-level DRM protection (Windows/macOS) and KWin Scripting (Linux KDE) makes the AI invisible to screen-sharing, screenshots, and meeting platforms (Zoom, Teams). -* **Voice Transcription**: Real-time system audio capture streamed to AssemblyAI for live results. +* **Voice Transcription**: Real-time system audio capture via browser WebRTC (Windows/macOS) and native zero-latency PipeWire integration (Linux) streamed to AssemblyAI. * **Screen OCR**: Local Tesseract.js extraction from any region-snip directly into your AI prompt. * **High-Stakes Personas**: 12 context-aware identities tailored for specific assessment scenarios. * **Multi-Provider AI**: Connect to the world's most powerful models through a unified interface. @@ -40,7 +40,7 @@ Imposter provides a discreet, always-on-top AI layer designed for maximum invisi Built on a specialized architecture, Imposter remains non-intrusive and computationally isolated until triggered. * **Hardware DRM**: Blocks all standard capture APIs on Windows and macOS. -* **KDE Plasma Stealth**: Native compositor integration via KWin scripting for true invisibility on Linux (KDE 6.6+). +* **KDE Plasma Stealth**: Native compositor integration via KWin scripting for true invisibility on Linux (KDE 6.6+). Hides from screen capture, taskbar, and alt-tab menus. * **Disguised UI**: Instantly transform into a standard utility identity to stay under the radar. * **100% Local**: Zero telemetry. Prompts, conversations, and keys never leave your machine. From 6e2b0ab05f1a0131b86d40b8b79a23a63e8e6d77 Mon Sep 17 00:00:00 2001 From: Dhriti Date: Sat, 18 Apr 2026 11:06:53 +0530 Subject: [PATCH 5/6] feat:kde --- src/main/transcription.js | 73 +++++++++++++++++++++++++++++++-------- src/renderer/island.html | 15 +++++--- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/main/transcription.js b/src/main/transcription.js index 51edcd5..5ba530a 100644 --- a/src/main/transcription.js +++ b/src/main/transcription.js @@ -1,6 +1,6 @@ const { WebSocket } = require('ws'); const { getMainWindow, getIslandWindow } = require('./window-manager'); -const { spawn } = require('child_process'); +const { spawn, execSync } = require('child_process'); const kdeManager = require('./kde-manager'); let assemblySocket = null; @@ -28,7 +28,7 @@ function startTranscription(apiKey) { isConnecting = true; try { - const url = `wss://streaming.assemblyai.com/v3/ws?sample_rate=${SAMPLE_RATE}&speech_model=u3-rt-pro`; + const url = `wss://streaming.assemblyai.com/v3/ws?sample_rate=${SAMPLE_RATE}&encoding=pcm_s16le&speech_model=u3-rt-pro`; assemblySocket = new WebSocket(url, { headers: { Authorization: apiKey } }); const connectionTimeout = setTimeout(() => { @@ -48,40 +48,81 @@ function startTranscription(apiKey) { // If we are on KDE/Linux, start the native PipeWire capture now if (kdeManager.isKdePlasma()) { - console.log('[TRANSCRIPTION] Starting native PipeWire audio capture (pw-record)'); + console.log('[TRANSCRIPTION] Starting native Linux audio capture (parec)'); try { - nativeAudioProcess = spawn('pw-record', [ + // Verify parec is installed + try { + execSync('which parec', { encoding: 'utf-8' }); + } catch (_) { + const msg = 'Voice capture requires "parec" (pulseaudio-utils). Install it:\n' + + ' Arch/EndeavourOS: sudo pacman -S libpulse\n' + + ' Ubuntu/Debian: sudo apt install pulseaudio-utils\n' + + ' Fedora: sudo dnf install pulseaudio-utils'; + console.error(`[TRANSCRIPTION] ${msg}`); + safeSendStatus('error', 'Missing dependency: parec. Check terminal for install instructions.'); + return; + } + // Dynamically get the exact monitor name of the default sink + let targetSink = '@DEFAULT_SINK@.monitor'; + try { + const defaultSink = execSync('pactl get-default-sink', { encoding: 'utf-8' }).trim(); + if (defaultSink) { + targetSink = `${defaultSink}.monitor`; + console.log(`[TRANSCRIPTION] Resolved target sink: ${targetSink}`); + } + } catch (e) { + console.warn('[TRANSCRIPTION] Could not dynamically resolve default sink.', e.message); + } + + // Use parec for guaranteed raw PCM output (no WAV headers) + nativeAudioProcess = spawn('parec', [ '--rate=16000', '--channels=1', - '--format=s16', - '-' // stdout + '--format=s16le', + '-d', targetSink ]); let audioBuffer = Buffer.alloc(0); - // 16000Hz * 1 channel * 2 bytes = 32000 bytes/sec. 100ms = 3200 bytes. - const MIN_CHUNK_SIZE = 3200; + // 100ms chunks: 16000Hz * 1ch * 2bytes * 0.1s = 3200 bytes + const CHUNK_SIZE = 3200; + // Software gain — monitor sinks capture at very low amplitude + const AUDIO_GAIN = 10; + + function amplifyPCM(buffer) { + const amplified = Buffer.alloc(buffer.length); + for (let i = 0; i < buffer.length - 1; i += 2) { + let sample = buffer.readInt16LE(i); + sample = Math.max(-32768, Math.min(32767, sample * AUDIO_GAIN)); + amplified.writeInt16LE(sample, i); + } + return amplified; + } nativeAudioProcess.stdout.on('data', (data) => { audioBuffer = Buffer.concat([audioBuffer, data]); - if (audioBuffer.length >= MIN_CHUNK_SIZE) { + // Drain buffer in proper-sized chunks (while, not if!) + while (audioBuffer.length >= CHUNK_SIZE) { + const chunk = audioBuffer.subarray(0, CHUNK_SIZE); + audioBuffer = audioBuffer.subarray(CHUNK_SIZE); + if (assemblySocket && assemblySocket.readyState === WebSocket.OPEN) { - assemblySocket.send(audioBuffer); + const boosted = amplifyPCM(chunk); + assemblySocket.send(boosted); } - audioBuffer = Buffer.alloc(0); } }); nativeAudioProcess.on('error', (err) => { - console.error('[TRANSCRIPTION] pw-record error:', err.message); + console.error('[TRANSCRIPTION] parec error:', err.message); }); nativeAudioProcess.on('close', (code) => { - console.log(`[TRANSCRIPTION] pw-record exited with code ${code}`); + console.log(`[TRANSCRIPTION] parec exited with code ${code}`); nativeAudioProcess = null; }); } catch (err) { - console.error('[TRANSCRIPTION] Failed to spawn pw-record:', err); + console.error('[TRANSCRIPTION] Failed to spawn parec:', err); } } }); @@ -94,6 +135,10 @@ function startTranscription(apiKey) { console.error('[TRANSCRIPTION] Server error:', data.error); } + if (data.type === 'Turn') { + console.log(`[TRANSCRIPTION] Received Turn: "${data.transcript}" (end_of_turn: ${data.end_of_turn})`); + } + const mainWindow = getMainWindow(); const islandWindow = getIslandWindow(); if (mainWindow && !mainWindow.isDestroyed()) { diff --git a/src/renderer/island.html b/src/renderer/island.html index d1a0af8..dd10dec 100644 --- a/src/renderer/island.html +++ b/src/renderer/island.html @@ -146,14 +146,21 @@ if (data.type === 'Turn') { const text = data.transcript || ''; if (!text) return; - subtitleEl.textContent = text; - subtitleEl.className = data.end_of_turn ? 'final' : 'partial'; - + if (data.end_of_turn) { + // Final sentence — show it styled as "final" and keep visible longer + subtitleEl.textContent = text; + subtitleEl.className = 'final'; + clearTimer = setTimeout(() => { subtitleEl.textContent = ''; + subtitleEl.className = 'partial'; island.classList.remove('active'); - }, 5000); + }, 6000); + } else { + // Still speaking — show partial in real-time + subtitleEl.textContent = text; + subtitleEl.className = 'partial'; } } }); From 0041e9dc1744521dd20e25060a5603d3b7492d28 Mon Sep 17 00:00:00 2001 From: Dhriti Date: Sat, 18 Apr 2026 14:15:28 +0530 Subject: [PATCH 6/6] feat:screenshot working --- src/main/shortcuts.js | 39 ++++++++++++++++++++++++++++++++------- src/renderer/js/api.js | 27 ++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/main/shortcuts.js b/src/main/shortcuts.js index 635a578..e01cda8 100644 --- a/src/main/shortcuts.js +++ b/src/main/shortcuts.js @@ -1,6 +1,9 @@ const { globalShortcut, app, screen, desktopCapturer } = require('electron'); const { getMainWindow, createSnipperWindow } = require('./window-manager'); const path = require('path'); +const { execSync } = require('child_process'); +const fs = require('fs'); +const kdeManager = require('./kde-manager'); function safeRegister(accelerator, callback) { try { @@ -65,13 +68,35 @@ function registerShortcuts() { const { width, height } = primaryDisplay.bounds; await new Promise(r => setTimeout(r, 100)); - const sources = await desktopCapturer.getSources({ - types: ['screen'], - thumbnailSize: { width: Math.max(width, 1920), height: Math.max(height, 1080) } - }); - - if (sources && sources.length > 0) { - const screenSource = sources[0].thumbnail.toDataURL(); + let screenSource = null; + + if (kdeManager.isKdePlasma()) { + // On KDE/Wayland, bypass desktopCapturer and the portal popup by using spectacle + const tmpPath = '/tmp/imposter_snip.png'; + try { + execSync(`spectacle -b -n -o ${tmpPath}`, { stdio: 'ignore' }); + const imgData = fs.readFileSync(tmpPath); + screenSource = `data:image/png;base64,${imgData.toString('base64')}`; + // Clean up the temporary file silently + fs.unlink(tmpPath, () => {}); + } catch (err) { + console.error('[SHORTCUT] Spectacle capture failed, falling back to desktopCapturer:', err.message); + } + } + + // Fallback to standard desktopCapturer for Windows/Mac (or if spectacle failed) + if (!screenSource) { + const sources = await desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { width: Math.max(width, 1920), height: Math.max(height, 1080) } + }); + + if (sources && sources.length > 0) { + screenSource = sources[0].thumbnail.toDataURL(); + } + } + + if (screenSource) { createSnipperWindow(path.join(__dirname, 'preload_snipper.js'), screenSource); } } catch (err) { diff --git a/src/renderer/js/api.js b/src/renderer/js/api.js index 60b0589..cca3f39 100644 --- a/src/renderer/js/api.js +++ b/src/renderer/js/api.js @@ -13,6 +13,27 @@ export async function fetchModels() { return null; } +function extractErrorMessage(result, providerName) { + if (!result) return 'Unknown error occurred'; + // If it's a provider JSON error object (e.g. Gemini) + if (typeof result.error === 'object' && result.error.message) return result.error.message; + // If it's an IPC handler or standard fetch error string + if (typeof result.message === 'string' && result.message.trim() !== '') { + try { + // Sometimes the message is raw HTML from a 503 page, so let's try parsing it as JSON first + const parsed = JSON.parse(result.message); + if (parsed.error && parsed.error.message) return parsed.error.message; + } catch (e) { + // It's not JSON, so it's probably raw HTML or plain text. If it's long HTML, truncate it. + if (result.message.includes(' 150) { + return `${providerName} is currently unavailable (Status: ${result.status}). Please try again in a moment.`; + } + return result.message; + } + } + return `${providerName} returned an error (status: ${result.status || 'unknown'})`; +} + export async function generateOllamaResponse(baseUrl, payload) { if (!baseUrl) throw new Error('No base URL configured for Ollama'); @@ -24,7 +45,7 @@ export async function generateOllamaResponse(baseUrl, payload) { }); if (result && result.error) { - throw new Error(result.message || `Ollama returned an error (status: ${result.status || 'unknown'})`); + throw new Error(extractErrorMessage(result, 'Ollama')); } return result || {}; @@ -47,7 +68,7 @@ export async function generateOpenRouterResponse(apiKey, payload) { }); if (result && result.error) { - throw new Error(result.message || `OpenRouter returned an error (status: ${result.status || 'unknown'})`); + throw new Error(extractErrorMessage(result, 'OpenRouter')); } return result || {}; @@ -104,7 +125,7 @@ export async function generateGeminiResponse(apiKey, modelId, conversationHistor }); if (result && result.error) { - throw new Error(result.error.message || `Gemini returned an error (status: ${result.status || 'unknown'})`); + throw new Error(extractErrorMessage(result, 'Gemini')); } return result || {};