Compare commits

20 Commits
Author SHA1 Message Date
Илья Глазунов b1d9130b50 chore: bump version to 0.7.8 and remove unused DMG options in forge.config.js
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-02-20 23:33:29 +03:00
Илья Глазунов 07c39455be fix: update release workflow to use macOS runner; bump version to 0.7.7
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-02-20 23:20:23 +03:00
Илья Глазунов 39fe8d948f feat: update DMG format to UDZO and add additional options; bump version to 0.7.6
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
2026-02-20 23:07:37 +03:00
Илья Глазунов 1330af8d19 feat: update DMG maker configuration and bump version to 0.7.5
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-02-20 22:54:56 +03:00
Илья Глазунов c161f251ed Update application logos: replace logo.icns, logo.ico, and logo.png with new versions
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-02-17 15:15:24 +03:00
Илья Глазунов d111b88886 fix: update badge colors for latest and stable version in README.md 2026-02-16 23:44:18 +03:00
Илья Глазунов 043b5d159e feat: update README.md with enhanced description and features; add logo image 2026-02-16 23:36:44 +03:00
Илья Глазунов 851edc6da1 feat: add .npmrc file with node-linker set to hoisted
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
2026-02-16 22:49:23 +03:00
Илья Глазунов c68a546e72 feat: add '@electron/rebuild' dependency version 3.7.2 to pnpm-lock.yaml
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-02-16 22:46:41 +03:00
Илья Глазунов 09b2530714 chore: bump version to 0.7.1 in package.json
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-02-16 22:44:22 +03:00
Илья Глазунов a9dce5bf3c fix: ensure current directory is included in pnpm workspace packages 2026-02-16 22:43:49 +03:00
Илья Глазунов 31d50c9713 Merge branch 'v0.7.0-update'
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
2026-02-16 22:40:20 +03:00
Shifty bbad79875c Merge pull request 'enhancment/code-highlighting-in-llm-chat' (#6) from enhancment/code-highlighting-in-llm-chat into v0.7.0-update
Reviewed-on: https://git.pyserve.org/Shifty/Mastermind/pulls/6
2026-02-16 19:32:13 +00:00
Илья Глазунов 7f15b65eb1 feat: add light theme support and update theme detection in renderer 2026-02-16 22:29:30 +03:00
Илья Глазунов d6dbaa3141 feat: add syntax highlighting for code blocks in AssistantView 2026-02-16 22:29:24 +03:00
Shifty 2ebde60dcd Merge pull request 'Fixing local transcription flow' (#5) from fix/local-transcription-flow into v0.7.0-update
Reviewed-on: https://git.pyserve.org/Shifty/Mastermind/pulls/5
2026-02-16 16:56:55 +00:00
Илья Глазунов 0d56e06724 feat: add whisper progress tracking and UI updates for download status 2026-02-16 19:55:39 +03:00
Илья Глазунов 526bc4e877 feat: enhance Whisper worker integration with system Node.js detection 2026-02-16 17:10:57 +03:00
Илья Глазунов 684b61755c feat: implement Whisper worker for isolated audio transcription 2026-02-16 11:38:26 +03:00
Илья Глазунов 430895d9ab small fixes 2026-02-13 22:11:01 +03:00
20 changed files with 5728 additions and 2132 deletions
+2 -1
View File
@@ -64,10 +64,11 @@ jobs:
out/make/**/*.deb out/make/**/*.deb
out/make/**/*.rpm out/make/**/*.rpm
if-no-files-found: ignore if-no-files-found: ignore
compression-level: 0
release: release:
needs: build needs: build
runs-on: ubuntu-latest runs-on: macos-latest
if: github.server_url == 'https://github.com' if: github.server_url == 'https://github.com'
permissions: permissions:
+1
View File
@@ -0,0 +1 @@
node-linker=hoisted
+259 -38
View File
@@ -1,58 +1,279 @@
<!-- <img width="1299" height="424" alt="cd (1)" src="https://github.com/user-attachments/assets/b25fff4d-043d-4f38-9985-f832ae0d0f6e" /> --> <div align="center">
# Mastermind <img src="assets/images/logo.png" alt="Mastermind Logo" width="200"/>
# Mastermind
### Your AI Assistant for High-Stakes Conversations
*Real-time contextual suggestions when you need them most*
[![Release](https://img.shields.io/github/actions/workflow/status/ShiftyX1/Mastermind/release.yml?label=release)](https://github.com/ShiftyX1/Mastermind/actions/workflows/release.yml)
[![License](https://img.shields.io/badge/license-GPL3.0-blue.svg)](LICENSE)
[![Latest Version](https://img.shields.io/github/v/release/ShiftyX1/Mastermind?include_prereleases&label=latest&color=FFFF00)](https://github.com/ShiftyX1/Mastermind/releases)
[![Stable Version](https://img.shields.io/github/v/release/ShiftyX1/Mastermind?color=6666FF)](https://github.com/ShiftyX1/Mastermind/releases)
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows-lightgrey.svg)](#requirements)
</div>
--- ---
> [!NOTE] ## What is Mastermind?
> Use latest MacOS and Windows version, older versions have limited support
> [!NOTE] Mastermind is an **AI assistant** for high-stakes conversations. Whether you're in a job interview, closing a deal, or navigating a complex negotiation, Mastermind analyzes what you see and hear in real-time, providing contextual suggestions and talking points to support your responses.
> During testing it wont answer if you ask something, you need to simulate interviewer asking question, which it will answer
A real-time AI assistant that provides contextual help during video calls, interviews, presentations, and meetings using screen capture and audio analysis. It is fork of [Cheating Daddy](https://github.com/sohzm/cheating-daddy) project. Think of it as having an experienced coach reviewing the conversation and offering suggestions, helping you recall relevant information and structure your thoughts more effectively. The AI provides support material—you still need to understand, adapt, and deliver the responses in your own words.
## Features > [!WARNING]
> **AI models can and do make mistakes.** Suggestions may contain errors, outdated information, or inappropriate content. This tool is designed to assist people who already have relevant knowledge and need help organizing their thoughts—not to fake expertise you don't possess. Always verify critical information and use your own judgment.
- **Live AI Assistance**: Real-time help powered by Gemini API / OpenAI SDK / OpenAI Realtime API, so you can choose which one you want to use ### The Hidden Assistant Advantage
- **Screen & Audio Capture**: Analyzes what you see and hear for contextual responses
- **Multiple Profiles**: Interview, Sales Call, Business Meeting, Presentation, Negotiation
- **Transparent Overlay**: Always-on-top window that can be positioned anywhere, if something goes wrong you can hide it without stoping session and losing context!
- **Click-through Mode**: Make window transparent to clicks when needed
- **Cross-platform**: Works on macOS, Windows, and Linux (kinda, dont use, just for testing rn)
## Setup Mastermind operates discreetly with a transparent overlay that blends into your screen. The system analyzes both visual content and audio in real-time, generating contextual suggestions within seconds. It adapts its suggestions based on your selected scenario—interview, sales, meeting, or presentation. Ghost mode allows you to interact with content behind the overlay without closing it.
1. **Get a API Key**: Visit [Google AI Studio](https://aistudio.google.com/apikey) or [OpenAI](https://platform.openai.com/docs/api-reference) or any other OpenAI-compatible API! **Remember:** This is an assistive tool, not a magic solution. It works best when you have genuine knowledge and need support organizing your thoughts under pressure.
2. **Install Dependencies**: `pnpm install`
3. **Run the App**: `pnpm start`
## Usage ## Key Features
1. Enter your API key in the main window, select provider and model you want to use in preferences ### Real-Time Multi-Modal Analysis
2. Choose your profile and language in settings Mastermind captures your screen and audio simultaneously, processing both visual content and audio streams to understand conversation context. It analyzes what's being discussed and generates relevant suggestions based on that context. The system supports dual-stream audio capture to distinguish between system audio and your microphone input, though transcription accuracy depends on audio quality, accents, and background noise.
3. Click "Start Session" to begin, if you want to use push-to-talk mode, you can enable it in preferences
4. Position the window using keyboard shortcuts, or use your mouse to move it ### Local-First Privacy Option
5. The AI will provide real-time assistance based on your screen and system audio/microphone input, you can also send text messages to AI by pressing Enter Choose between cloud AI providers for maximum performance or run everything locally with Ollama integration and offline Whisper.js transcription. When using local processing, no audio or screen data ever leaves your machine. The local transcription engine uses ONNX Runtime with GPU acceleration support for fast, private speech-to-text conversion.
### Conversation History & Context
Mastermind saves conversation turns during the session, building context as the conversation progresses. You can view session history and export conversations for later review. The AI uses this accumulated context to provide more relevant suggestions as it learns about the discussion topic—though the quality of contextual understanding is limited by the AI model's capabilities and the clarity of the captured audio/screen content.
### Specialized Profiles
Mastermind comes with six pre-configured personas designed for different scenarios:
**Job Interview** — Suggested responses to technical and behavioral questions, STAR method frameworks, and structured talking points based on your background.
**Sales Call** — Objection handling suggestions, closing technique ideas, pricing strategy considerations, and rapport building approaches.
**Business Meeting** — Data-driven talking points, strategic recommendations, and action-oriented communication suggestions.
**Presentation** — Fact-checking support, audience engagement ideas, and recovery suggestions for unexpected situations.
**Negotiation** — Tactical considerations, counter-offer frameworks, and strategic talking points to support your position.
**Exam Assistant** — Information lookup and answer suggestions for exam questions, optimized for quick reference.
### Invisible Design
The transparent overlay stays on top without blocking your view, with keyboard-driven positioning for quick adjustments. You can hide the window instantly with one click if needed, and customize opacity to match your environment perfectly.
### Flexible AI Backend
Mastermind supports multiple AI providers and can work with both cloud and local models:
**Google Gemini** — Fast, cost-effective multimodal processing with excellent vision capabilities. Supports Gemini 2.0 Flash with real-time API for ultra-low latency responses.
**OpenAI** — Industry-leading language understanding with GPT-4 and GPT-4o models.
**Groq** — High-speed inference with competitive pricing and excellent performance.
**Ollama** — Run completely local AI models on your machine for full privacy. No data ever leaves your device.
**Any OpenAI-Compatible API** — Connect to LocalAI, LM Studio, or any custom endpoint that follows the OpenAI API format.
---
## Advanced Features
**Response Modes** — Toggle between Brief mode (1-3 sentences, optimal for quick glances) and Detailed mode (comprehensive explanations with full context) based on your needs during the session.
**Google Search Integration** — Optional real-time web search capability allows the AI to fetch current information. Note that search results may include outdated or incorrect information—always verify critical facts from authoritative sources.
**Custom System Prompts** — Tailor the AI's behavior with custom instructions specific to your industry, role, or situation. Add your resume, company information, or specialized knowledge to improve context relevance.
**Multi-Language Support** — Works in 30+ languages including English, Spanish, German, French, Japanese, Korean, Chinese, Hindi, Arabic, and many more. Auto-detection available for multilingual conversations, though accuracy varies by language and accent.
**Customizable Keyboard Shortcuts** — Every shortcut can be remapped to your preference. Create your own workflow that feels natural to you.
---
## Getting Started
### Installation
#### For Users (Recommended)
Download the latest release for your platform from the [GitHub Releases](https://github.com/ShiftyX1/Mastermind/releases) page:
**macOS:**
1. Download `Mastermind-[version].dmg`
2. Open the DMG file and drag Mastermind to your Applications folder
3. Launch Mastermind from Applications (you may need to allow the app in System Preferences → Security & Privacy on first launch)
**Windows:**
1. Download `Mastermind-[version]-Setup.exe`
2. Run the installer and follow the setup wizard
3. Launch Mastermind from the Start menu or desktop shortcut
#### For Developers
If you want to build from source or contribute to development:
```bash
# Clone the repository
git clone https://github.com/ShiftyX1/Mastermind.git
cd Mastermind
# Install dependencies
pnpm install
# Launch in development mode
pnpm start
# Build distributable packages (DMG for macOS, Setup.exe for Windows)
pnpm run make
# Package without creating installers
pnpm run package
```
### First-Time Setup
**Get Your AI Key:** Start by obtaining an API key from [Google AI Studio](https://aistudio.google.com/apikey) (recommended for beginners), [OpenAI Platform](https://platform.openai.com/api-keys), [Groq Console](https://console.groq.com), or configure a local Ollama instance for complete privacy.
**Configure Your Assistant:** Enter your API key in the main window and select your preferred AI provider and model. Choose your primary use case profile from the six available scenarios. Select your language or use Auto for multilingual support.
**Start Your Session:** Click "Start Session" to activate your hidden assistant. Grant screen recording and audio capture permissions when prompted by your system. Position the overlay window where it's most useful and adjust opacity to blend naturally with your environment.
### Daily Usage
**Starting a Session:** Select the appropriate profile for your scenario—Interview, Sales, Meeting, Presentation, Negotiation, or Exam. Adjust opacity and position to blend naturally with your environment. Choose your audio mode: Speaker Only (system audio), Microphone Only, or Both for dual-stream capture.
**During Your Conversation:** The AI analyzes your screen and audio context in real-time, with suggestions appearing in the overlay. You can type questions directly for clarification and use keyboard shortcuts to reposition or hide the window. Toggle between brief and detailed response modes depending on your needs.
**Important:** Treat AI suggestions as reference material, not verified facts. Quickly scan suggestions, extract useful points, and deliver responses in your own words with your own understanding. Don't read AI responses verbatim—this often sounds unnatural and may include errors.
**Pro Tips:** Position the window in your natural eye-line to avoid obvious glances. Use click-through mode when you need to interact with content behind the overlay. Keep sessions focused on one topic for better context. Enable local transcription when working with sensitive information.
---
## Keyboard Shortcuts ## Keyboard Shortcuts
Master these shortcuts for seamless, discreet operation:
| Action | Shortcut | Purpose |
|--------|----------|---------|
| **Move Window** | `Ctrl/Cmd + Arrow Keys` | Reposition without using mouse |
| **Toggle Click-Through** | `Ctrl/Cmd + M` | Make window transparent to clicks |
| **Quick Hide** | `Ctrl/Cmd + \` | Instantly hide/show or go back |
| **Send Message** | `Enter` | Send text query to AI |
| **Quick Position** | Custom | Set your favorite window positions |
> **Pro Tip**: All shortcuts are fully customizable in settings. Create your own stealth workflow!
---
## Audio Capture Technology
Mastermind uses advanced audio capture to understand conversations in real-time, with support for both cloud and local transcription.
**macOS** — Leverages [SystemAudioDump](https://github.com/sohzm/systemAudioDump) for crystal-clear system audio capture. Supports three modes: Speaker Only (system audio), Microphone Only (your voice), or Both (simultaneous dual-stream capture).
**Windows** — Professional loopback audio capture for system sounds, with full microphone support and dual-stream capabilities for capturing both sides of the conversation.
**Linux** — Microphone input support. System audio capture is currently in development.
**Local Transcription** — Built-in offline speech-to-text using Whisper.js powered by ONNX Runtime. Your audio is processed locally on your machine without sending data to external services. Supports GPU acceleration on compatible hardware.
---
## Use Cases
### Job Interviews
Get suggested responses and frameworks for technical questions, helping you structure your thoughts using proven methods like STAR. Mastermind can help you recall relevant examples from your background and organize talking points, but you need to adapt and deliver them authentically in your own voice.
### Sales & Client Calls
Access reference material for objection handling and competitive positioning. The system can suggest talking points and strategies by analyzing the conversation context, but closing deals requires genuine understanding of your product and the client's needs—AI suggestions are starting points, not scripts to read verbatim.
### Business Negotiations
Receive strategic considerations and framework suggestions based on the conversation flow. Mastermind can help you structure counter-offers and identify discussion points, but successful negotiation requires reading the room, building rapport, and making judgment calls that AI cannot make for you.
### Presentations & Demos
Get quick fact-checking and audience engagement ideas during your presentation. If questions arise, Mastermind can suggest relevant information, but you should verify accuracy and ensure you genuinely understand what you're presenting—especially important for technical content where deep knowledge is expected.
---
## System Requirements
| Component | Requirement |
|-----------|-------------|
| **Operating System** | macOS 10.15+, Windows 10/11 (latest versions recommended) |
| **Permissions** | Screen recording, audio capture (system audio and/or microphone) |
| **Internet** | Required for cloud AI providers (Gemini, OpenAI, Groq). Optional for local Ollama models |
| **AI Provider** | API key from Gemini, OpenAI, Groq, or local Ollama installation |
| **For Local Transcription** | 4GB+ RAM recommended, GPU acceleration optional but recommended |
**Current Version:** 0.7.3
> [!NOTE] > [!NOTE]
> All keyboard shortcuts are customizable in settings. You can check some default shortcuts below. > **Platform Support**: macOS and Windows are fully supported and tested. Linux support is experimental.
- **Window Movement**: `Ctrl/Cmd + Arrow Keys` - Move window > [!TIP]
- **Click-through**: `Ctrl/Cmd + M` - Toggle mouse events > **Testing Mode**: When testing, simulate someone asking you questions. The AI responds to detected questions rather than your own queries.
- **Close/Back**: `Ctrl/Cmd + \` - Close window or go back
- **Send Message**: `Enter` - Send text to AI
## Audio Capture ---
- **macOS**: [SystemAudioDump](https://github.com/Mohammed-Yasin-Mulla/Sound) for system audio capture, you can use microphone input as well ## Known Limitations
- **Windows**: Loopback audio capture, you can use microphone input as well
- **Linux**: Microphone input
## Requirements **AI Response Quality** — AI models can make mistakes, provide outdated information, or misinterpret context. Always verify critical information and use suggestions as supporting material, not absolute truth. The quality of responses depends heavily on the AI model you choose and the context you provide.
- Electron-compatible OS (macOS, Windows, Linux) **Not a Replacement for Knowledge** — Mastermind is a tool to help you recall and structure information, not to replace your actual expertise. The most effective use is when you already understand the subject matter and need help articulating or remembering specific details.
- AI Provider API key
- Screen recording permissions **Linux Support** — System audio capture is not yet implemented on Linux. Only microphone input is currently supported.
- Microphone/audio permissions
**Local Transcription Performance** — First-time usage requires downloading the Whisper model files (approximately 150MB). Transcription speed depends on your hardware; GPU acceleration is recommended for optimal performance.
**macOS Permissions** — Screen recording and audio capture require explicit system permissions. You may need to restart the app after granting permissions on first launch.
**Session Context** — The AI maintains context only within the current session. Starting a new session clears previous conversation history (though history can be saved and viewed later).
---
## Privacy & Ethics
**Your Data, Your Control:** All audio and screen capture happens locally on your device. API communications are direct and clear between you and your chosen provider. For complete privacy, you can use local AI models through Ollama without any data leaving your machine.
**Critical Disclaimers:**
- AI models can generate incorrect, biased, or inappropriate content. Always verify important information from authoritative sources.
- This tool provides suggestions, not verified facts. You are responsible for the accuracy of what you say.
- Relying entirely on AI suggestions without understanding the content can backfire—especially in technical or expert conversations where follow-up questions will reveal lack of genuine knowledge.
**Responsible Use:**
Mastermind is designed as a preparation and cognitive support tool—like having notes or a reference guide. It works best when you already have foundational knowledge and need help organizing thoughts or recalling details under pressure.
**Ethical Boundaries:** Always comply with the rules and policies of your specific context. Many situations explicitly prohibit external assistance:
- Academic exams and certification tests typically ban any form of external help
- Some professional interviews and assessments prohibit such tools
- Certain regulated industries have strict rules about information access during calls
- Using AI assistance where prohibited can result in serious consequences, including job loss or legal issues
**Use this tool to support your genuine expertise, not to fake knowledge you don't have.** The best outcomes happen when AI assists someone who understands the subject, not when it replaces actual competence.
---
## Contributing
Based on the excellent work from [Cheating Daddy](https://github.com/sohzm/cheating-daddy).
Contributions are welcome! Please see [AGENTS.md](AGENTS.md) for development guidelines.
---
## License
This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details.
---
<div align="center">
### A tool to support your expertise, not replace it
*Use responsibly. Verify information. Understand what you're saying.*
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

+81 -73
View File
@@ -1,78 +1,86 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses'); const { FusesPlugin } = require("@electron-forge/plugin-fuses");
const { FuseV1Options, FuseVersion } = require('@electron/fuses'); const { FuseV1Options, FuseVersion } = require("@electron/fuses");
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
asar: { asar: {
unpack: '**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**', unpack:
}, "**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**",
extraResource: ['./src/assets/SystemAudioDump'],
name: 'Mastermind',
icon: 'src/assets/logo',
// use `security find-identity -v -p codesigning` to find your identity
// for macos signing
// also fuck apple
// osxSign: {
// identity: '<paste your identity here>',
// optionsForFile: (filePath) => {
// return {
// entitlements: 'entitlements.plist',
// };
// },
// },
// notarize if off cuz i ran this for 6 hours and it still didnt finish
// osxNotarize: {
// appleId: 'your apple id',
// appleIdPassword: 'app specific password',
// teamId: 'your team id',
// },
}, },
rebuildConfig: {}, extraResource: ["./src/assets/SystemAudioDump"],
makers: [ name: "Mastermind",
{ icon: "src/assets/logo",
name: '@electron-forge/maker-squirrel', // use `security find-identity -v -p codesigning` to find your identity
config: { // for macos signing
name: 'mastermind', // also fuck apple
productName: 'Mastermind', // osxSign: {
shortcutName: 'Mastermind', // identity: '<paste your identity here>',
createDesktopShortcut: true, // optionsForFile: (filePath) => {
createStartMenuShortcut: true, // return {
}, // entitlements: 'entitlements.plist',
// };
// },
// },
// notarize if off cuz i ran this for 6 hours and it still didnt finish
// osxNotarize: {
// appleId: 'your apple id',
// appleIdPassword: 'app specific password',
// teamId: 'your team id',
// },
},
rebuildConfig: {
// Ensure onnxruntime-node is rebuilt against Electron's Node.js headers
// so the native binding matches the ABI used in packaged builds.
onlyModules: ["onnxruntime-node", "sharp"],
},
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
name: "mastermind",
productName: "Mastermind",
shortcutName: "Mastermind",
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
},
{
name: "@electron-forge/maker-dmg",
platforms: ["darwin"],
config: {
icon: "src/assets/logo.icns",
}
},
{
name: "@reforged/maker-appimage",
platforms: ["linux"],
config: {
options: {
name: "Mastermind",
productName: "Mastermind",
genericName: "AI Assistant",
description: "AI assistant for interviews and learning",
categories: ["Development", "Education"],
icon: "src/assets/logo.png",
}, },
{ },
name: '@electron-forge/maker-dmg', },
platforms: ['darwin'], ],
}, plugins: [
{ {
name: '@reforged/maker-appimage', name: "@electron-forge/plugin-auto-unpack-natives",
platforms: ['linux'], config: {},
config: { },
options: { // Fuses are used to enable/disable various Electron functionality
name: 'Mastermind', // at package time, before code signing the application
productName: 'Mastermind', new FusesPlugin({
genericName: 'AI Assistant', version: FuseVersion.V1,
description: 'AI assistant for interviews and learning', [FuseV1Options.RunAsNode]: false,
categories: ['Development', 'Education'], [FuseV1Options.EnableCookieEncryption]: true,
icon: 'src/assets/logo.png' [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
} [FuseV1Options.EnableNodeCliInspectArguments]: false,
}, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
}, [FuseV1Options.OnlyLoadAppFromAsar]: true,
], }),
plugins: [ ],
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
}; };
+53 -50
View File
@@ -1,53 +1,56 @@
{ {
"name": "mastermind", "name": "mastermind",
"productName": "Mastermind", "productName": "Mastermind",
"version": "0.7.0", "version": "0.7.8",
"description": "Mastermind AI assistant", "description": "Mastermind AI assistant",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"start": "electron-forge start", "start": "electron-forge start",
"package": "electron-forge package", "package": "electron-forge package",
"make": "electron-forge make", "make": "electron-forge make",
"publish": "electron-forge publish", "publish": "electron-forge publish",
"lint": "echo \"No linting configured\"" "lint": "echo \"No linting configured\"",
}, "postinstall": "electron-rebuild -f -w onnxruntime-node"
"keywords": [ },
"mastermind", "keywords": [
"mastermind ai", "mastermind",
"mastermind ai assistant", "mastermind ai",
"mastermind ai assistant for interviews", "mastermind ai assistant",
"mastermind ai assistant for interviews" "mastermind ai assistant for interviews",
], "mastermind ai assistant for interviews"
"author": { ],
"name": "ShiftyX1", "author": {
"email": "lead@pyserve.org" "name": "ShiftyX1",
}, "email": "lead@pyserve.org"
"license": "GPL-3.0", },
"dependencies": { "license": "GPL-3.0",
"@google/genai": "^1.41.0", "dependencies": {
"@huggingface/transformers": "^3.8.1", "@google/genai": "^1.41.0",
"electron-squirrel-startup": "^1.0.1", "@huggingface/transformers": "^3.8.1",
"ollama": "^0.6.3", "electron-squirrel-startup": "^1.0.1",
"openai": "^6.22.0", "ollama": "^0.6.3",
"p-retry": "^4.6.2", "openai": "^6.22.0",
"ws": "^8.19.0" "p-retry": "^4.6.2",
}, "ws": "^8.19.0"
"devDependencies": { },
"@electron-forge/cli": "^7.8.1", "devDependencies": {
"@electron-forge/maker-deb": "^7.8.1", "@electron-forge/cli": "^7.8.1",
"@electron-forge/maker-dmg": "^7.8.1", "@electron-forge/maker-deb": "^7.8.1",
"@electron-forge/maker-rpm": "^7.8.1", "@electron-forge/maker-dmg": "^7.8.1",
"@electron-forge/maker-squirrel": "^7.8.1", "@electron-forge/maker-rpm": "^7.8.1",
"@electron-forge/maker-zip": "^7.8.1", "@electron-forge/maker-squirrel": "^7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1", "@electron-forge/maker-zip": "^7.8.1",
"@electron-forge/plugin-fuses": "^7.8.1", "@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
"@electron/fuses": "^1.8.0", "@electron-forge/plugin-fuses": "^7.8.1",
"@reforged/maker-appimage": "^5.0.0", "@electron/fuses": "^1.8.0",
"electron": "^30.0.5" "@electron/rebuild": "^3.7.1",
}, "@reforged/maker-appimage": "^5.0.0",
"pnpm": { "electron": "^30.0.5",
"overrides": { "electron-icon-builder": "^2.0.1"
"p-retry": "4.6.2" },
} "pnpm": {
"overrides": {
"p-retry": "4.6.2"
} }
}
} }
+1660 -3
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -1,3 +1,6 @@
packages:
- '.'
onlyBuiltDependencies: onlyBuiltDependencies:
- electron - electron
- electron-winstaller - electron-winstaller
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 190 KiB

+8
View File
@@ -382,6 +382,7 @@ export class CheatingDaddyApp extends LitElement {
_storageLoaded: { state: true }, _storageLoaded: { state: true },
_updateAvailable: { state: true }, _updateAvailable: { state: true },
_whisperDownloading: { state: true }, _whisperDownloading: { state: true },
_whisperProgress: { state: true },
}; };
constructor() { constructor() {
@@ -407,6 +408,7 @@ export class CheatingDaddyApp extends LitElement {
this._timerInterval = null; this._timerInterval = null;
this._updateAvailable = false; this._updateAvailable = false;
this._whisperDownloading = false; this._whisperDownloading = false;
this._whisperProgress = null;
this._localVersion = ""; this._localVersion = "";
this._loadFromStorage(); this._loadFromStorage();
@@ -485,6 +487,10 @@ export class CheatingDaddyApp extends LitElement {
); );
ipcRenderer.on("whisper-downloading", (_, downloading) => { ipcRenderer.on("whisper-downloading", (_, downloading) => {
this._whisperDownloading = downloading; this._whisperDownloading = downloading;
if (!downloading) this._whisperProgress = null;
});
ipcRenderer.on("whisper-progress", (_, progress) => {
this._whisperProgress = progress;
}); });
} }
} }
@@ -500,6 +506,7 @@ export class CheatingDaddyApp extends LitElement {
ipcRenderer.removeAllListeners("click-through-toggled"); ipcRenderer.removeAllListeners("click-through-toggled");
ipcRenderer.removeAllListeners("reconnect-failed"); ipcRenderer.removeAllListeners("reconnect-failed");
ipcRenderer.removeAllListeners("whisper-downloading"); ipcRenderer.removeAllListeners("whisper-downloading");
ipcRenderer.removeAllListeners("whisper-progress");
} }
} }
@@ -778,6 +785,7 @@ export class CheatingDaddyApp extends LitElement {
.onStart=${() => this.handleStart()} .onStart=${() => this.handleStart()}
.onExternalLink=${(url) => this.handleExternalLinkClick(url)} .onExternalLink=${(url) => this.handleExternalLinkClick(url)}
.whisperDownloading=${this._whisperDownloading} .whisperDownloading=${this._whisperDownloading}
.whisperProgress=${this._whisperProgress}
></main-view> ></main-view>
`; `;
+280 -1
View File
@@ -110,11 +110,220 @@ export class AssistantView extends LitElement {
padding: var(--space-md); padding: var(--space-md);
overflow-x: auto; overflow-x: auto;
margin: 0.8em 0; margin: 0.8em 0;
position: relative;
}
.response-container pre::before {
content: attr(data-language);
position: absolute;
top: 0;
right: 0;
background: var(--bg-elevated);
color: var(--text-secondary);
padding: 4px 12px;
font-size: var(--font-size-xs);
font-family: var(--font-mono);
border: 1px solid var(--border);
border-top: none;
border-right: none;
border-bottom-left-radius: var(--radius-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
} }
.response-container pre code { .response-container pre code {
background: none; background: none;
padding: 0; padding: 0;
font-family: var(--font-mono);
font-size: 0.9em;
line-height: 1.5;
color: var(--text-primary);
}
/* ── Syntax highlighting for code blocks ── */
/* Default (Dark theme) */
.response-container .hljs {
color: #c9d1d9;
background: transparent;
}
.response-container .hljs-doctag,
.response-container .hljs-keyword,
.response-container .hljs-meta .hljs-keyword,
.response-container .hljs-template-tag,
.response-container .hljs-template-variable,
.response-container .hljs-type,
.response-container .hljs-variable.language_ {
color: #ff7b72;
}
.response-container .hljs-title,
.response-container .hljs-title.class_,
.response-container .hljs-title.class_.inherited__,
.response-container .hljs-title.function_ {
color: #d2a8ff;
}
.response-container .hljs-attr,
.response-container .hljs-attribute,
.response-container .hljs-literal,
.response-container .hljs-meta,
.response-container .hljs-number,
.response-container .hljs-operator,
.response-container .hljs-selector-attr,
.response-container .hljs-selector-class,
.response-container .hljs-selector-id,
.response-container .hljs-variable {
color: #79c0ff;
}
.response-container .hljs-meta .hljs-string,
.response-container .hljs-regexp,
.response-container .hljs-string {
color: #a5d6ff;
}
.response-container .hljs-built_in,
.response-container .hljs-symbol {
color: #ffa657;
}
.response-container .hljs-code,
.response-container .hljs-comment,
.response-container .hljs-formula {
color: #8b949e;
}
.response-container .hljs-name,
.response-container .hljs-quote,
.response-container .hljs-selector-pseudo,
.response-container .hljs-selector-tag {
color: #7ee787;
}
.response-container .hljs-subst {
color: #c9d1d9;
}
.response-container .hljs-section {
color: #1f6feb;
font-weight: 700;
}
.response-container .hljs-bullet {
color: #f2cc60;
}
.response-container .hljs-emphasis {
color: #c9d1d9;
font-style: italic;
}
.response-container .hljs-strong {
color: #c9d1d9;
font-weight: 700;
}
.response-container .hljs-addition {
color: #aff5b4;
background-color: #033a16;
}
.response-container .hljs-deletion {
color: #ffdcd7;
background-color: #67060c;
}
/* Light theme syntax highlighting */
:host-context(body[data-theme-type="light"]) .response-container .hljs {
color: #24292f;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-doctag,
:host-context(body[data-theme-type="light"]) .response-container .hljs-keyword,
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-keyword,
:host-context(body[data-theme-type="light"]) .response-container .hljs-template-tag,
:host-context(body[data-theme-type="light"]) .response-container .hljs-template-variable,
:host-context(body[data-theme-type="light"]) .response-container .hljs-type,
:host-context(body[data-theme-type="light"]) .response-container .hljs-variable.language_ {
color: #cf222e;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-title,
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_,
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_.inherited__,
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.function_ {
color: #8250df;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-attr,
:host-context(body[data-theme-type="light"]) .response-container .hljs-attribute,
:host-context(body[data-theme-type="light"]) .response-container .hljs-literal,
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta,
:host-context(body[data-theme-type="light"]) .response-container .hljs-number,
:host-context(body[data-theme-type="light"]) .response-container .hljs-operator,
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-attr,
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-class,
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-id,
:host-context(body[data-theme-type="light"]) .response-container .hljs-variable {
color: #0550ae;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-string,
:host-context(body[data-theme-type="light"]) .response-container .hljs-regexp,
:host-context(body[data-theme-type="light"]) .response-container .hljs-string {
color: #0a3069;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-built_in,
:host-context(body[data-theme-type="light"]) .response-container .hljs-symbol {
color: #953800;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-code,
:host-context(body[data-theme-type="light"]) .response-container .hljs-comment,
:host-context(body[data-theme-type="light"]) .response-container .hljs-formula {
color: #6e7781;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-name,
:host-context(body[data-theme-type="light"]) .response-container .hljs-quote,
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-pseudo,
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-tag {
color: #116329;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-subst {
color: #24292f;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-section {
color: #0969da;
font-weight: 700;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-bullet {
color: #953800;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-emphasis {
color: #24292f;
font-style: italic;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-strong {
color: #24292f;
font-weight: 700;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-addition {
color: #116329;
background-color: #dafbe1;
}
:host-context(body[data-theme-type="light"]) .response-container .hljs-deletion {
color: #82071e;
background-color: #ffebe9;
} }
.response-container a { .response-container a {
@@ -402,10 +611,33 @@ export class AssistantView extends LitElement {
renderMarkdown(content) { renderMarkdown(content) {
if (typeof window !== "undefined" && window.marked) { if (typeof window !== "undefined" && window.marked) {
try { try {
// Configure marked to use highlight.js for syntax highlighting
window.marked.setOptions({ window.marked.setOptions({
breaks: true, breaks: true,
gfm: true, gfm: true,
sanitize: false, sanitize: false,
highlight: (code, lang) => {
if (window.hljs && lang) {
try {
return window.hljs.highlight(code, { language: lang }).value;
} catch (e) {
// If language is not recognized, try auto-detection
try {
return window.hljs.highlightAuto(code).value;
} catch (err) {
return window.hljs.escapeHtml(code);
}
}
} else if (window.hljs) {
// Auto-detect language if not specified
try {
return window.hljs.highlightAuto(code).value;
} catch (e) {
return window.hljs.escapeHtml(code);
}
}
return code;
},
}); });
let rendered = window.marked.parse(content); let rendered = window.marked.parse(content);
rendered = this.wrapWordsInSpans(rendered); rendered = this.wrapWordsInSpans(rendered);
@@ -421,7 +653,7 @@ export class AssistantView extends LitElement {
wrapWordsInSpans(html) { wrapWordsInSpans(html) {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html"); const doc = parser.parseFromString(html, "text/html");
const tagsToSkip = ["PRE"]; const tagsToSkip = ["PRE", "CODE"];
function wrap(node) { function wrap(node) {
if ( if (
@@ -453,6 +685,49 @@ export class AssistantView extends LitElement {
return doc.body.innerHTML; return doc.body.innerHTML;
} }
applyCodeHighlighting(container) {
if (!window.hljs) return;
// Find all code blocks in the rendered content
const codeBlocks = container.querySelectorAll("pre code");
codeBlocks.forEach((block) => {
const pre = block.parentElement;
if (!pre || pre.tagName !== "PRE") return;
// Skip if already highlighted
if (block.classList.contains("hljs")) {
return;
}
const code = block.textContent;
let lang = block.className.replace(/language-|lang-/, "") || "";
try {
if (lang && window.hljs.getLanguage(lang)) {
block.innerHTML = window.hljs.highlight(code, { language: lang }).value;
} else {
// Auto-detect language
const result = window.hljs.highlightAuto(code);
block.innerHTML = result.value;
if (result.language && !lang) {
lang = result.language;
block.className = `language-${lang}`;
}
}
block.classList.add("hljs");
// Set data-language attribute on pre tag for display
if (lang) {
pre.setAttribute("data-language", lang);
}
} catch (e) {
console.warn("Error highlighting code block:", e);
// Leave block as-is if highlighting fails
}
});
}
navigateToPreviousResponse() { navigateToPreviousResponse() {
if (this.currentResponseIndex > 0) { if (this.currentResponseIndex > 0) {
this.currentResponseIndex--; this.currentResponseIndex--;
@@ -775,6 +1050,10 @@ export class AssistantView extends LitElement {
const currentResponse = this.getCurrentResponse(); const currentResponse = this.getCurrentResponse();
const renderedResponse = this.renderMarkdown(currentResponse); const renderedResponse = this.renderMarkdown(currentResponse);
container.innerHTML = renderedResponse; container.innerHTML = renderedResponse;
// Apply syntax highlighting to code blocks
this.applyCodeHighlighting(container);
if (this.shouldAnimateResponse) { if (this.shouldAnimateResponse) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("response-animation-complete", { new CustomEvent("response-animation-complete", {
File diff suppressed because it is too large Load Diff
+292 -255
View File
@@ -1,299 +1,336 @@
if (require('electron-squirrel-startup')) { if (require("electron-squirrel-startup")) {
process.exit(0); process.exit(0);
} }
const { app, BrowserWindow, shell, ipcMain } = require('electron'); // ── Global crash handlers to prevent silent process termination ──
const { createWindow, updateGlobalShortcuts } = require('./utils/window'); process.on("uncaughtException", (error) => {
const { setupGeminiIpcHandlers, stopMacOSAudioCapture, sendToRenderer } = require('./utils/gemini'); console.error("[FATAL] Uncaught exception:", error);
const storage = require('./storage'); try {
const { sendToRenderer } = require("./utils/gemini");
sendToRenderer(
"update-status",
"Fatal error: " + (error?.message || "unknown"),
);
} catch (_) {
// sendToRenderer may not be available yet
}
});
process.on("unhandledRejection", (reason) => {
console.error("[FATAL] Unhandled promise rejection:", reason);
try {
const { sendToRenderer } = require("./utils/gemini");
sendToRenderer(
"update-status",
"Unhandled error: " +
(reason instanceof Error ? reason.message : String(reason)),
);
} catch (_) {
// sendToRenderer may not be available yet
}
});
const { app, BrowserWindow, shell, ipcMain } = require("electron");
const { createWindow, updateGlobalShortcuts } = require("./utils/window");
const {
setupGeminiIpcHandlers,
stopMacOSAudioCapture,
sendToRenderer,
} = require("./utils/gemini");
const storage = require("./storage");
const geminiSessionRef = { current: null }; const geminiSessionRef = { current: null };
let mainWindow = null; let mainWindow = null;
function createMainWindow() { function createMainWindow() {
mainWindow = createWindow(sendToRenderer, geminiSessionRef); mainWindow = createWindow(sendToRenderer, geminiSessionRef);
return mainWindow; return mainWindow;
} }
app.whenReady().then(async () => { app.whenReady().then(async () => {
// Initialize storage (checks version, resets if needed) // Initialize storage (checks version, resets if needed)
storage.initializeStorage(); storage.initializeStorage();
// Trigger screen recording permission prompt on macOS if not already granted // Trigger screen recording permission prompt on macOS if not already granted
if (process.platform === 'darwin') { if (process.platform === "darwin") {
const { desktopCapturer } = require('electron'); const { desktopCapturer } = require("electron");
desktopCapturer.getSources({ types: ['screen'] }).catch(() => {}); desktopCapturer.getSources({ types: ["screen"] }).catch(() => {});
} }
createMainWindow();
setupGeminiIpcHandlers(geminiSessionRef);
setupStorageIpcHandlers();
setupGeneralIpcHandlers();
});
app.on("window-all-closed", () => {
stopMacOSAudioCapture();
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("before-quit", () => {
stopMacOSAudioCapture();
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow(); createMainWindow();
setupGeminiIpcHandlers(geminiSessionRef); }
setupStorageIpcHandlers();
setupGeneralIpcHandlers();
});
app.on('window-all-closed', () => {
stopMacOSAudioCapture();
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
stopMacOSAudioCapture();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
}); });
function setupStorageIpcHandlers() { function setupStorageIpcHandlers() {
// ============ CONFIG ============ // ============ CONFIG ============
ipcMain.handle('storage:get-config', async () => { ipcMain.handle("storage:get-config", async () => {
try { try {
return { success: true, data: storage.getConfig() }; return { success: true, data: storage.getConfig() };
} catch (error) { } catch (error) {
console.error('Error getting config:', error); console.error("Error getting config:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:set-config', async (event, config) => { ipcMain.handle("storage:set-config", async (event, config) => {
try { try {
storage.setConfig(config); storage.setConfig(config);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error setting config:', error); console.error("Error setting config:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:update-config', async (event, key, value) => { ipcMain.handle("storage:update-config", async (event, key, value) => {
try { try {
storage.updateConfig(key, value); storage.updateConfig(key, value);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error updating config:', error); console.error("Error updating config:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ CREDENTIALS ============ // ============ CREDENTIALS ============
ipcMain.handle('storage:get-credentials', async () => { ipcMain.handle("storage:get-credentials", async () => {
try { try {
return { success: true, data: storage.getCredentials() }; return { success: true, data: storage.getCredentials() };
} catch (error) { } catch (error) {
console.error('Error getting credentials:', error); console.error("Error getting credentials:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:set-credentials', async (event, credentials) => { ipcMain.handle("storage:set-credentials", async (event, credentials) => {
try { try {
storage.setCredentials(credentials); storage.setCredentials(credentials);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error setting credentials:', error); console.error("Error setting credentials:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:get-api-key', async () => { ipcMain.handle("storage:get-api-key", async () => {
try { try {
return { success: true, data: storage.getApiKey() }; return { success: true, data: storage.getApiKey() };
} catch (error) { } catch (error) {
console.error('Error getting API key:', error); console.error("Error getting API key:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:set-api-key', async (event, apiKey) => { ipcMain.handle("storage:set-api-key", async (event, apiKey) => {
try { try {
storage.setApiKey(apiKey); storage.setApiKey(apiKey);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error setting API key:', error); console.error("Error setting API key:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:get-groq-api-key', async () => { ipcMain.handle("storage:get-groq-api-key", async () => {
try { try {
return { success: true, data: storage.getGroqApiKey() }; return { success: true, data: storage.getGroqApiKey() };
} catch (error) { } catch (error) {
console.error('Error getting Groq API key:', error); console.error("Error getting Groq API key:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:set-groq-api-key', async (event, groqApiKey) => { ipcMain.handle("storage:set-groq-api-key", async (event, groqApiKey) => {
try { try {
storage.setGroqApiKey(groqApiKey); storage.setGroqApiKey(groqApiKey);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error setting Groq API key:', error); console.error("Error setting Groq API key:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ PREFERENCES ============ // ============ PREFERENCES ============
ipcMain.handle('storage:get-preferences', async () => { ipcMain.handle("storage:get-preferences", async () => {
try { try {
return { success: true, data: storage.getPreferences() }; return { success: true, data: storage.getPreferences() };
} catch (error) { } catch (error) {
console.error('Error getting preferences:', error); console.error("Error getting preferences:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:set-preferences', async (event, preferences) => { ipcMain.handle("storage:set-preferences", async (event, preferences) => {
try { try {
storage.setPreferences(preferences); storage.setPreferences(preferences);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error setting preferences:', error); console.error("Error setting preferences:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:update-preference', async (event, key, value) => { ipcMain.handle("storage:update-preference", async (event, key, value) => {
try { try {
storage.updatePreference(key, value); storage.updatePreference(key, value);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error updating preference:', error); console.error("Error updating preference:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ KEYBINDS ============ // ============ KEYBINDS ============
ipcMain.handle('storage:get-keybinds', async () => { ipcMain.handle("storage:get-keybinds", async () => {
try { try {
return { success: true, data: storage.getKeybinds() }; return { success: true, data: storage.getKeybinds() };
} catch (error) { } catch (error) {
console.error('Error getting keybinds:', error); console.error("Error getting keybinds:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:set-keybinds', async (event, keybinds) => { ipcMain.handle("storage:set-keybinds", async (event, keybinds) => {
try { try {
storage.setKeybinds(keybinds); storage.setKeybinds(keybinds);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error setting keybinds:', error); console.error("Error setting keybinds:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ HISTORY ============ // ============ HISTORY ============
ipcMain.handle('storage:get-all-sessions', async () => { ipcMain.handle("storage:get-all-sessions", async () => {
try { try {
return { success: true, data: storage.getAllSessions() }; return { success: true, data: storage.getAllSessions() };
} catch (error) { } catch (error) {
console.error('Error getting sessions:', error); console.error("Error getting sessions:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:get-session', async (event, sessionId) => { ipcMain.handle("storage:get-session", async (event, sessionId) => {
try { try {
return { success: true, data: storage.getSession(sessionId) }; return { success: true, data: storage.getSession(sessionId) };
} catch (error) { } catch (error) {
console.error('Error getting session:', error); console.error("Error getting session:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:save-session', async (event, sessionId, data) => { ipcMain.handle("storage:save-session", async (event, sessionId, data) => {
try { try {
storage.saveSession(sessionId, data); storage.saveSession(sessionId, data);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error saving session:', error); console.error("Error saving session:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:delete-session', async (event, sessionId) => { ipcMain.handle("storage:delete-session", async (event, sessionId) => {
try { try {
storage.deleteSession(sessionId); storage.deleteSession(sessionId);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error deleting session:', error); console.error("Error deleting session:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('storage:delete-all-sessions', async () => { ipcMain.handle("storage:delete-all-sessions", async () => {
try { try {
storage.deleteAllSessions(); storage.deleteAllSessions();
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error deleting all sessions:', error); console.error("Error deleting all sessions:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ LIMITS ============ // ============ LIMITS ============
ipcMain.handle('storage:get-today-limits', async () => { ipcMain.handle("storage:get-today-limits", async () => {
try { try {
return { success: true, data: storage.getTodayLimits() }; return { success: true, data: storage.getTodayLimits() };
} catch (error) { } catch (error) {
console.error('Error getting today limits:', error); console.error("Error getting today limits:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ CLEAR ALL ============ // ============ CLEAR ALL ============
ipcMain.handle('storage:clear-all', async () => { ipcMain.handle("storage:clear-all", async () => {
try { try {
storage.clearAllData(); storage.clearAllData();
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error clearing all data:', error); console.error("Error clearing all data:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
} }
function setupGeneralIpcHandlers() { function setupGeneralIpcHandlers() {
ipcMain.handle('get-app-version', async () => { ipcMain.handle("get-app-version", async () => {
return app.getVersion(); return app.getVersion();
}); });
ipcMain.handle('quit-application', async event => { ipcMain.handle("quit-application", async (event) => {
try { try {
stopMacOSAudioCapture(); stopMacOSAudioCapture();
app.quit(); app.quit();
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error quitting application:', error); console.error("Error quitting application:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('open-external', async (event, url) => { ipcMain.handle("open-external", async (event, url) => {
try { try {
await shell.openExternal(url); await shell.openExternal(url);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error opening external URL:', error); console.error("Error opening external URL:", error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.on('update-keybinds', (event, newKeybinds) => { ipcMain.on("update-keybinds", (event, newKeybinds) => {
if (mainWindow) { if (mainWindow) {
// Also save to storage // Also save to storage
storage.setKeybinds(newKeybinds); storage.setKeybinds(newKeybinds);
updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef); updateGlobalShortcuts(
} newKeybinds,
}); mainWindow,
sendToRenderer,
geminiSessionRef,
);
}
});
// Debug logging from renderer // Debug logging from renderer
ipcMain.on('log-message', (event, msg) => { ipcMain.on("log-message", (event, msg) => {
console.log(msg); console.log(msg);
}); });
} }
+364 -344
View File
@@ -1,554 +1,574 @@
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const os = require('os'); const os = require("os");
const CONFIG_VERSION = 1; const CONFIG_VERSION = 1;
// Default values // Default values
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
configVersion: CONFIG_VERSION, configVersion: CONFIG_VERSION,
onboarded: false, onboarded: false,
layout: 'normal' layout: "normal",
}; };
const DEFAULT_CREDENTIALS = { const DEFAULT_CREDENTIALS = {
apiKey: '', apiKey: "",
groqApiKey: '', groqApiKey: "",
openaiCompatibleApiKey: '', openaiCompatibleApiKey: "",
openaiCompatibleBaseUrl: '', openaiCompatibleBaseUrl: "",
openaiCompatibleModel: '' openaiCompatibleModel: "",
}; };
const DEFAULT_PREFERENCES = { const DEFAULT_PREFERENCES = {
customPrompt: '', customPrompt: "",
selectedProfile: 'interview', selectedProfile: "interview",
selectedLanguage: 'en-US', selectedLanguage: "en-US",
selectedScreenshotInterval: '5', selectedScreenshotInterval: "5",
selectedImageQuality: 'medium', selectedImageQuality: "medium",
advancedMode: false, advancedMode: false,
audioMode: 'speaker_only', audioMode: "speaker_only",
fontSize: 'medium', fontSize: "medium",
backgroundTransparency: 0.8, backgroundTransparency: 0.8,
googleSearchEnabled: false, googleSearchEnabled: false,
responseProvider: 'gemini', responseProvider: "gemini",
ollamaHost: 'http://127.0.0.1:11434', ollamaHost: "http://127.0.0.1:11434",
ollamaModel: 'llama3.1', ollamaModel: "llama3.1",
whisperModel: 'Xenova/whisper-small', whisperModel: "Xenova/whisper-small",
whisperDevice: "", // '' = auto-detect, 'cpu' = native, 'wasm' = compatible
}; };
const DEFAULT_KEYBINDS = null; // null means use system defaults const DEFAULT_KEYBINDS = null; // null means use system defaults
const DEFAULT_LIMITS = { const DEFAULT_LIMITS = {
data: [] // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } } data: [], // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
}; };
// Get the config directory path based on OS // Get the config directory path based on OS
function getConfigDir() { function getConfigDir() {
const platform = os.platform(); const platform = os.platform();
let configDir; let configDir;
if (platform === 'win32') { if (platform === "win32") {
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config'); configDir = path.join(
} else if (platform === 'darwin') { os.homedir(),
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'cheating-daddy-config'); "AppData",
} else { "Roaming",
configDir = path.join(os.homedir(), '.config', 'cheating-daddy-config'); "cheating-daddy-config",
} );
} else if (platform === "darwin") {
configDir = path.join(
os.homedir(),
"Library",
"Application Support",
"cheating-daddy-config",
);
} else {
configDir = path.join(os.homedir(), ".config", "cheating-daddy-config");
}
return configDir; return configDir;
} }
// File paths // File paths
function getConfigPath() { function getConfigPath() {
return path.join(getConfigDir(), 'config.json'); return path.join(getConfigDir(), "config.json");
} }
function getCredentialsPath() { function getCredentialsPath() {
return path.join(getConfigDir(), 'credentials.json'); return path.join(getConfigDir(), "credentials.json");
} }
function getPreferencesPath() { function getPreferencesPath() {
return path.join(getConfigDir(), 'preferences.json'); return path.join(getConfigDir(), "preferences.json");
} }
function getKeybindsPath() { function getKeybindsPath() {
return path.join(getConfigDir(), 'keybinds.json'); return path.join(getConfigDir(), "keybinds.json");
} }
function getLimitsPath() { function getLimitsPath() {
return path.join(getConfigDir(), 'limits.json'); return path.join(getConfigDir(), "limits.json");
} }
function getHistoryDir() { function getHistoryDir() {
return path.join(getConfigDir(), 'history'); return path.join(getConfigDir(), "history");
} }
// Helper to read JSON file safely // Helper to read JSON file safely
function readJsonFile(filePath, defaultValue) { function readJsonFile(filePath, defaultValue) {
try { try {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath, 'utf8'); const data = fs.readFileSync(filePath, "utf8");
return JSON.parse(data); return JSON.parse(data);
}
} catch (error) {
console.warn(`Error reading ${filePath}:`, error.message);
} }
return defaultValue; } catch (error) {
console.warn(`Error reading ${filePath}:`, error.message);
}
return defaultValue;
} }
// Helper to write JSON file safely // Helper to write JSON file safely
function writeJsonFile(filePath, data) { function writeJsonFile(filePath, data) {
try { try {
const dir = path.dirname(filePath); const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (error) {
console.error(`Error writing ${filePath}:`, error.message);
return false;
} }
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
return true;
} catch (error) {
console.error(`Error writing ${filePath}:`, error.message);
return false;
}
} }
// Check if we need to reset (no configVersion or wrong version) // Check if we need to reset (no configVersion or wrong version)
function needsReset() { function needsReset() {
const configPath = getConfigPath(); const configPath = getConfigPath();
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
return true; return true;
} }
try { try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
return !config.configVersion || config.configVersion !== CONFIG_VERSION; return !config.configVersion || config.configVersion !== CONFIG_VERSION;
} catch { } catch {
return true; return true;
} }
} }
// Wipe and reinitialize the config directory // Wipe and reinitialize the config directory
function resetConfigDir() { function resetConfigDir() {
const configDir = getConfigDir(); const configDir = getConfigDir();
console.log('Resetting config directory...'); console.log("Resetting config directory...");
// Remove existing directory if it exists // Remove existing directory if it exists
if (fs.existsSync(configDir)) { if (fs.existsSync(configDir)) {
fs.rmSync(configDir, { recursive: true, force: true }); fs.rmSync(configDir, { recursive: true, force: true });
} }
// Create fresh directory structure // Create fresh directory structure
fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(getHistoryDir(), { recursive: true }); fs.mkdirSync(getHistoryDir(), { recursive: true });
// Initialize with defaults // Initialize with defaults
writeJsonFile(getConfigPath(), DEFAULT_CONFIG); writeJsonFile(getConfigPath(), DEFAULT_CONFIG);
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS); writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES); writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
console.log('Config directory initialized with defaults'); console.log("Config directory initialized with defaults");
} }
// Initialize storage - call this on app startup // Initialize storage - call this on app startup
function initializeStorage() { function initializeStorage() {
if (needsReset()) { if (needsReset()) {
resetConfigDir(); resetConfigDir();
} else { } else {
// Ensure history directory exists // Ensure history directory exists
const historyDir = getHistoryDir(); const historyDir = getHistoryDir();
if (!fs.existsSync(historyDir)) { if (!fs.existsSync(historyDir)) {
fs.mkdirSync(historyDir, { recursive: true }); fs.mkdirSync(historyDir, { recursive: true });
}
} }
}
} }
// ============ CONFIG ============ // ============ CONFIG ============
function getConfig() { function getConfig() {
return readJsonFile(getConfigPath(), DEFAULT_CONFIG); return readJsonFile(getConfigPath(), DEFAULT_CONFIG);
} }
function setConfig(config) { function setConfig(config) {
const current = getConfig(); const current = getConfig();
const updated = { ...current, ...config, configVersion: CONFIG_VERSION }; const updated = { ...current, ...config, configVersion: CONFIG_VERSION };
return writeJsonFile(getConfigPath(), updated); return writeJsonFile(getConfigPath(), updated);
} }
function updateConfig(key, value) { function updateConfig(key, value) {
const config = getConfig(); const config = getConfig();
config[key] = value; config[key] = value;
return writeJsonFile(getConfigPath(), config); return writeJsonFile(getConfigPath(), config);
} }
// ============ CREDENTIALS ============ // ============ CREDENTIALS ============
function getCredentials() { function getCredentials() {
return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS); return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
} }
function setCredentials(credentials) { function setCredentials(credentials) {
const current = getCredentials(); const current = getCredentials();
const updated = { ...current, ...credentials }; const updated = { ...current, ...credentials };
return writeJsonFile(getCredentialsPath(), updated); return writeJsonFile(getCredentialsPath(), updated);
} }
function getApiKey() { function getApiKey() {
return getCredentials().apiKey || ''; return getCredentials().apiKey || "";
} }
function setApiKey(apiKey) { function setApiKey(apiKey) {
return setCredentials({ apiKey }); return setCredentials({ apiKey });
} }
function getGroqApiKey() { function getGroqApiKey() {
return getCredentials().groqApiKey || ''; return getCredentials().groqApiKey || "";
} }
function setGroqApiKey(groqApiKey) { function setGroqApiKey(groqApiKey) {
return setCredentials({ groqApiKey }); return setCredentials({ groqApiKey });
} }
function getOpenAICompatibleConfig() { function getOpenAICompatibleConfig() {
const creds = getCredentials(); const creds = getCredentials();
return { return {
apiKey: creds.openaiCompatibleApiKey || '', apiKey: creds.openaiCompatibleApiKey || "",
baseUrl: creds.openaiCompatibleBaseUrl || '', baseUrl: creds.openaiCompatibleBaseUrl || "",
model: creds.openaiCompatibleModel || '' model: creds.openaiCompatibleModel || "",
}; };
} }
function setOpenAICompatibleConfig(apiKey, baseUrl, model) { function setOpenAICompatibleConfig(apiKey, baseUrl, model) {
return setCredentials({ return setCredentials({
openaiCompatibleApiKey: apiKey, openaiCompatibleApiKey: apiKey,
openaiCompatibleBaseUrl: baseUrl, openaiCompatibleBaseUrl: baseUrl,
openaiCompatibleModel: model openaiCompatibleModel: model,
}); });
} }
// ============ PREFERENCES ============ // ============ PREFERENCES ============
function getPreferences() { function getPreferences() {
const saved = readJsonFile(getPreferencesPath(), {}); const saved = readJsonFile(getPreferencesPath(), {});
return { ...DEFAULT_PREFERENCES, ...saved }; return { ...DEFAULT_PREFERENCES, ...saved };
} }
function setPreferences(preferences) { function setPreferences(preferences) {
const current = getPreferences(); const current = getPreferences();
const updated = { ...current, ...preferences }; const updated = { ...current, ...preferences };
return writeJsonFile(getPreferencesPath(), updated); return writeJsonFile(getPreferencesPath(), updated);
} }
function updatePreference(key, value) { function updatePreference(key, value) {
const preferences = getPreferences(); const preferences = getPreferences();
preferences[key] = value; preferences[key] = value;
return writeJsonFile(getPreferencesPath(), preferences); return writeJsonFile(getPreferencesPath(), preferences);
} }
// ============ KEYBINDS ============ // ============ KEYBINDS ============
function getKeybinds() { function getKeybinds() {
return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS); return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS);
} }
function setKeybinds(keybinds) { function setKeybinds(keybinds) {
return writeJsonFile(getKeybindsPath(), keybinds); return writeJsonFile(getKeybindsPath(), keybinds);
} }
// ============ LIMITS (Rate Limiting) ============ // ============ LIMITS (Rate Limiting) ============
function getLimits() { function getLimits() {
return readJsonFile(getLimitsPath(), DEFAULT_LIMITS); return readJsonFile(getLimitsPath(), DEFAULT_LIMITS);
} }
function setLimits(limits) { function setLimits(limits) {
return writeJsonFile(getLimitsPath(), limits); return writeJsonFile(getLimitsPath(), limits);
} }
function getTodayDateString() { function getTodayDateString() {
const now = new Date(); const now = new Date();
return now.toISOString().split('T')[0]; // YYYY-MM-DD return now.toISOString().split("T")[0]; // YYYY-MM-DD
} }
function getTodayLimits() { function getTodayLimits() {
const limits = getLimits(); const limits = getLimits();
const today = getTodayDateString(); const today = getTodayDateString();
// Find today's entry // Find today's entry
const todayEntry = limits.data.find(entry => entry.date === today); const todayEntry = limits.data.find((entry) => entry.date === today);
if (todayEntry) { if (todayEntry) {
// ensure new fields exist // ensure new fields exist
if(!todayEntry.groq) { if (!todayEntry.groq) {
todayEntry.groq = { todayEntry.groq = {
'qwen3-32b': { chars: 0, limit: 1500000 }, "qwen3-32b": { chars: 0, limit: 1500000 },
'gpt-oss-120b': { chars: 0, limit: 600000 }, "gpt-oss-120b": { chars: 0, limit: 600000 },
'gpt-oss-20b': { chars: 0, limit: 600000 }, "gpt-oss-20b": { chars: 0, limit: 600000 },
'kimi-k2-instruct': { chars: 0, limit: 600000 } "kimi-k2-instruct": { chars: 0, limit: 600000 },
}; };
} }
if(!todayEntry.gemini) { if (!todayEntry.gemini) {
todayEntry.gemini = { todayEntry.gemini = {
'gemma-3-27b-it': { chars: 0 } "gemma-3-27b-it": { chars: 0 },
}; };
}
setLimits(limits);
return todayEntry;
} }
// No entry for today - clean old entries and create new one
limits.data = limits.data.filter(entry => entry.date === today);
const newEntry = {
date: today,
flash: { count: 0 },
flashLite: { count: 0 },
groq: {
'qwen3-32b': { chars: 0, limit: 1500000 },
'gpt-oss-120b': { chars: 0, limit: 600000 },
'gpt-oss-20b': { chars: 0, limit: 600000 },
'kimi-k2-instruct': { chars: 0, limit: 600000 }
},
gemini: {
'gemma-3-27b-it': { chars: 0 }
}
};
limits.data.push(newEntry);
setLimits(limits); setLimits(limits);
return todayEntry;
}
return newEntry; // No entry for today - clean old entries and create new one
limits.data = limits.data.filter((entry) => entry.date === today);
const newEntry = {
date: today,
flash: { count: 0 },
flashLite: { count: 0 },
groq: {
"qwen3-32b": { chars: 0, limit: 1500000 },
"gpt-oss-120b": { chars: 0, limit: 600000 },
"gpt-oss-20b": { chars: 0, limit: 600000 },
"kimi-k2-instruct": { chars: 0, limit: 600000 },
},
gemini: {
"gemma-3-27b-it": { chars: 0 },
},
};
limits.data.push(newEntry);
setLimits(limits);
return newEntry;
} }
function incrementLimitCount(model) { function incrementLimitCount(model) {
const limits = getLimits(); const limits = getLimits();
const today = getTodayDateString(); const today = getTodayDateString();
// Find or create today's entry // Find or create today's entry
let todayEntry = limits.data.find(entry => entry.date === today); let todayEntry = limits.data.find((entry) => entry.date === today);
if (!todayEntry) { if (!todayEntry) {
// Clean old entries and create new one // Clean old entries and create new one
limits.data = []; limits.data = [];
todayEntry = { todayEntry = {
date: today, date: today,
flash: { count: 0 }, flash: { count: 0 },
flashLite: { count: 0 } flashLite: { count: 0 },
}; };
limits.data.push(todayEntry); limits.data.push(todayEntry);
} else { } else {
// Clean old entries, keep only today // Clean old entries, keep only today
limits.data = limits.data.filter(entry => entry.date === today); limits.data = limits.data.filter((entry) => entry.date === today);
} }
// Increment the appropriate model count // Increment the appropriate model count
if (model === 'gemini-2.5-flash') { if (model === "gemini-2.5-flash") {
todayEntry.flash.count++; todayEntry.flash.count++;
} else if (model === 'gemini-2.5-flash-lite') { } else if (model === "gemini-2.5-flash-lite") {
todayEntry.flashLite.count++; todayEntry.flashLite.count++;
} }
setLimits(limits); setLimits(limits);
return todayEntry; return todayEntry;
} }
function incrementCharUsage(provider, model, charCount) { function incrementCharUsage(provider, model, charCount) {
getTodayLimits(); getTodayLimits();
const limits = getLimits(); const limits = getLimits();
const today = getTodayDateString(); const today = getTodayDateString();
const todayEntry = limits.data.find(entry => entry.date === today); const todayEntry = limits.data.find((entry) => entry.date === today);
if(todayEntry[provider] && todayEntry[provider][model]) { if (todayEntry[provider] && todayEntry[provider][model]) {
todayEntry[provider][model].chars += charCount; todayEntry[provider][model].chars += charCount;
setLimits(limits); setLimits(limits);
} }
return todayEntry; return todayEntry;
} }
function getAvailableModel() { function getAvailableModel() {
const todayLimits = getTodayLimits(); const todayLimits = getTodayLimits();
// RPD limits: flash = 20, flash-lite = 20 // RPD limits: flash = 20, flash-lite = 20
// After both exhausted, fall back to flash (for paid API users) // After both exhausted, fall back to flash (for paid API users)
if (todayLimits.flash.count < 20) { if (todayLimits.flash.count < 20) {
return 'gemini-2.5-flash'; return "gemini-2.5-flash";
} else if (todayLimits.flashLite.count < 20) { } else if (todayLimits.flashLite.count < 20) {
return 'gemini-2.5-flash-lite'; return "gemini-2.5-flash-lite";
} }
return 'gemini-2.5-flash'; // Default to flash for paid API users return "gemini-2.5-flash"; // Default to flash for paid API users
} }
function getModelForToday() { function getModelForToday() {
const todayEntry = getTodayLimits(); const todayEntry = getTodayLimits();
const groq = todayEntry.groq; const groq = todayEntry.groq;
if (groq['qwen3-32b'].chars < groq['qwen3-32b'].limit) { if (groq["qwen3-32b"].chars < groq["qwen3-32b"].limit) {
return 'qwen/qwen3-32b'; return "qwen/qwen3-32b";
} }
if (groq['gpt-oss-120b'].chars < groq['gpt-oss-120b'].limit) { if (groq["gpt-oss-120b"].chars < groq["gpt-oss-120b"].limit) {
return 'openai/gpt-oss-120b'; return "openai/gpt-oss-120b";
} }
if (groq['gpt-oss-20b'].chars < groq['gpt-oss-20b'].limit) { if (groq["gpt-oss-20b"].chars < groq["gpt-oss-20b"].limit) {
return 'openai/gpt-oss-20b'; return "openai/gpt-oss-20b";
} }
if (groq['kimi-k2-instruct'].chars < groq['kimi-k2-instruct'].limit) { if (groq["kimi-k2-instruct"].chars < groq["kimi-k2-instruct"].limit) {
return 'moonshotai/kimi-k2-instruct'; return "moonshotai/kimi-k2-instruct";
} }
// All limits exhausted // All limits exhausted
return null; return null;
} }
// ============ HISTORY ============ // ============ HISTORY ============
function getSessionPath(sessionId) { function getSessionPath(sessionId) {
return path.join(getHistoryDir(), `${sessionId}.json`); return path.join(getHistoryDir(), `${sessionId}.json`);
} }
function saveSession(sessionId, data) { function saveSession(sessionId, data) {
const sessionPath = getSessionPath(sessionId); const sessionPath = getSessionPath(sessionId);
// Load existing session to preserve metadata // Load existing session to preserve metadata
const existingSession = readJsonFile(sessionPath, null); const existingSession = readJsonFile(sessionPath, null);
const sessionData = { const sessionData = {
sessionId, sessionId,
createdAt: existingSession?.createdAt || parseInt(sessionId), createdAt: existingSession?.createdAt || parseInt(sessionId),
lastUpdated: Date.now(), lastUpdated: Date.now(),
// Profile context - set once when session starts // Profile context - set once when session starts
profile: data.profile || existingSession?.profile || null, profile: data.profile || existingSession?.profile || null,
customPrompt: data.customPrompt || existingSession?.customPrompt || null, customPrompt: data.customPrompt || existingSession?.customPrompt || null,
// Conversation data // Conversation data
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [], conversationHistory:
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || [] data.conversationHistory || existingSession?.conversationHistory || [],
}; screenAnalysisHistory:
return writeJsonFile(sessionPath, sessionData); data.screenAnalysisHistory ||
existingSession?.screenAnalysisHistory ||
[],
};
return writeJsonFile(sessionPath, sessionData);
} }
function getSession(sessionId) { function getSession(sessionId) {
return readJsonFile(getSessionPath(sessionId), null); return readJsonFile(getSessionPath(sessionId), null);
} }
function getAllSessions() { function getAllSessions() {
const historyDir = getHistoryDir(); const historyDir = getHistoryDir();
try { try {
if (!fs.existsSync(historyDir)) { if (!fs.existsSync(historyDir)) {
return []; return [];
}
const files = fs.readdirSync(historyDir)
.filter(f => f.endsWith('.json'))
.sort((a, b) => {
// Sort by timestamp descending (newest first)
const tsA = parseInt(a.replace('.json', ''));
const tsB = parseInt(b.replace('.json', ''));
return tsB - tsA;
});
return files.map(file => {
const sessionId = file.replace('.json', '');
const data = readJsonFile(path.join(historyDir, file), null);
if (data) {
return {
sessionId,
createdAt: data.createdAt,
lastUpdated: data.lastUpdated,
messageCount: data.conversationHistory?.length || 0,
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
profile: data.profile || null,
customPrompt: data.customPrompt || null
};
}
return null;
}).filter(Boolean);
} catch (error) {
console.error('Error reading sessions:', error.message);
return [];
} }
const files = fs
.readdirSync(historyDir)
.filter((f) => f.endsWith(".json"))
.sort((a, b) => {
// Sort by timestamp descending (newest first)
const tsA = parseInt(a.replace(".json", ""));
const tsB = parseInt(b.replace(".json", ""));
return tsB - tsA;
});
return files
.map((file) => {
const sessionId = file.replace(".json", "");
const data = readJsonFile(path.join(historyDir, file), null);
if (data) {
return {
sessionId,
createdAt: data.createdAt,
lastUpdated: data.lastUpdated,
messageCount: data.conversationHistory?.length || 0,
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
profile: data.profile || null,
customPrompt: data.customPrompt || null,
};
}
return null;
})
.filter(Boolean);
} catch (error) {
console.error("Error reading sessions:", error.message);
return [];
}
} }
function deleteSession(sessionId) { function deleteSession(sessionId) {
const sessionPath = getSessionPath(sessionId); const sessionPath = getSessionPath(sessionId);
try { try {
if (fs.existsSync(sessionPath)) { if (fs.existsSync(sessionPath)) {
fs.unlinkSync(sessionPath); fs.unlinkSync(sessionPath);
return true; return true;
}
} catch (error) {
console.error('Error deleting session:', error.message);
} }
return false; } catch (error) {
console.error("Error deleting session:", error.message);
}
return false;
} }
function deleteAllSessions() { function deleteAllSessions() {
const historyDir = getHistoryDir(); const historyDir = getHistoryDir();
try { try {
if (fs.existsSync(historyDir)) { if (fs.existsSync(historyDir)) {
const files = fs.readdirSync(historyDir).filter(f => f.endsWith('.json')); const files = fs
files.forEach(file => { .readdirSync(historyDir)
fs.unlinkSync(path.join(historyDir, file)); .filter((f) => f.endsWith(".json"));
}); files.forEach((file) => {
} fs.unlinkSync(path.join(historyDir, file));
return true; });
} catch (error) {
console.error('Error deleting all sessions:', error.message);
return false;
} }
return true;
} catch (error) {
console.error("Error deleting all sessions:", error.message);
return false;
}
} }
// ============ CLEAR ALL DATA ============ // ============ CLEAR ALL DATA ============
function clearAllData() { function clearAllData() {
resetConfigDir(); resetConfigDir();
return true; return true;
} }
module.exports = { module.exports = {
// Initialization // Initialization
initializeStorage, initializeStorage,
getConfigDir, getConfigDir,
// Config // Config
getConfig, getConfig,
setConfig, setConfig,
updateConfig, updateConfig,
// Credentials // Credentials
getCredentials, getCredentials,
setCredentials, setCredentials,
getApiKey, getApiKey,
setApiKey, setApiKey,
getGroqApiKey, getGroqApiKey,
setGroqApiKey, setGroqApiKey,
getOpenAICompatibleConfig, getOpenAICompatibleConfig,
setOpenAICompatibleConfig, setOpenAICompatibleConfig,
// Preferences // Preferences
getPreferences, getPreferences,
setPreferences, setPreferences,
updatePreference, updatePreference,
// Keybinds // Keybinds
getKeybinds, getKeybinds,
setKeybinds, setKeybinds,
// Limits (Rate Limiting) // Limits (Rate Limiting)
getLimits, getLimits,
setLimits, setLimits,
getTodayLimits, getTodayLimits,
incrementLimitCount, incrementLimitCount,
getAvailableModel, getAvailableModel,
incrementCharUsage, incrementCharUsage,
getModelForToday, getModelForToday,
// History // History
saveSession, saveSession,
getSession, getSession,
getAllSessions, getAllSessions,
deleteSession, deleteSession,
deleteAllSessions, deleteAllSessions,
// Clear all // Clear all
clearAllData clearAllData,
}; };
+732 -315
View File
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
/**
* nodeDetect.js Locate the system Node.js binary.
*
* When spawning child processes that rely on native addons compiled against the
* system Node.js ABI (e.g. onnxruntime-node), we must NOT run them inside
* Electron's embedded Node.js runtime the ABI mismatch causes SIGTRAP /
* SIGSEGV crashes. This module finds the real system `node` binary so we can
* pass it as `execPath` to `child_process.fork()`.
*
* Falls back to `null` when no system Node.js is found, letting the caller
* decide on an alternative strategy (e.g. WASM backend).
*/
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");
/** Well-known Node.js install locations per platform. */
const KNOWN_PATHS = {
darwin: [
"/usr/local/bin/node",
"/opt/homebrew/bin/node", // Apple Silicon Homebrew
path.join(os.homedir(), ".nvm/versions/node"), // nvm — needs glob
path.join(os.homedir(), ".volta/bin/node"), // Volta
path.join(os.homedir(), ".fnm/aliases/default/bin/node"), // fnm
path.join(os.homedir(), ".mise/shims/node"), // mise (rtx)
path.join(os.homedir(), ".asdf/shims/node"), // asdf
],
linux: [
"/usr/bin/node",
"/usr/local/bin/node",
path.join(os.homedir(), ".nvm/versions/node"),
path.join(os.homedir(), ".volta/bin/node"),
path.join(os.homedir(), ".fnm/aliases/default/bin/node"),
path.join(os.homedir(), ".mise/shims/node"),
path.join(os.homedir(), ".asdf/shims/node"),
],
win32: [
"C:\\Program Files\\nodejs\\node.exe",
"C:\\Program Files (x86)\\nodejs\\node.exe",
path.join(os.homedir(), "AppData", "Roaming", "nvm", "current", "node.exe"),
path.join(os.homedir(), ".volta", "bin", "node.exe"),
],
};
/**
* Find the latest nvm-installed Node.js binary on macOS / Linux.
* Returns the path to the `node` binary or null.
*/
function findNvmNode() {
const nvmDir = path.join(os.homedir(), ".nvm", "versions", "node");
try {
if (!fs.existsSync(nvmDir)) return null;
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith("v"));
if (versions.length === 0) return null;
// Sort semver descending (rough but sufficient)
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
const nodeBin = path.join(nvmDir, versions[0], "bin", "node");
if (fs.existsSync(nodeBin)) return nodeBin;
} catch (_) {
// Ignore
}
return null;
}
/**
* Attempt to resolve `node` via the system PATH using `which` (Unix) or
* `where` (Windows). Returns the path string or null.
*/
function whichNode() {
try {
const cmd = process.platform === "win32" ? "where node" : "which node";
const result = execSync(cmd, {
encoding: "utf8",
timeout: 5000,
env: {
...process.env,
// Ensure common manager shim dirs are on PATH
PATH: [
process.env.PATH || "",
"/usr/local/bin",
"/opt/homebrew/bin",
path.join(os.homedir(), ".volta", "bin"),
path.join(os.homedir(), ".fnm", "aliases", "default", "bin"),
path.join(os.homedir(), ".mise", "shims"),
path.join(os.homedir(), ".asdf", "shims"),
].join(process.platform === "win32" ? ";" : ":"),
},
stdio: ["ignore", "pipe", "ignore"],
}).trim();
// `where` on Windows may return multiple lines — take the first
const first = result.split(/\r?\n/)[0].trim();
if (first && fs.existsSync(first)) return first;
} catch (_) {
// Command failed
}
return null;
}
/**
* Check whether a given path is a real Node.js binary (not the Electron binary
* pretending to be Node via ELECTRON_RUN_AS_NODE).
*/
function isRealNode(nodePath) {
if (!nodePath) return false;
try {
const out = execSync(
`"${nodePath}" -e "process.stdout.write(String(!process.versions.electron))"`,
{
encoding: "utf8",
timeout: 5000,
env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined },
stdio: ["ignore", "pipe", "ignore"],
},
).trim();
return out === "true";
} catch (_) {
return false;
}
}
/**
* Find the system Node.js binary.
*
* @returns {{ nodePath: string } | null} The absolute path to system `node`,
* or null if none found. The caller should fall back to WASM when null.
*/
function findSystemNode() {
// 1. Try `which node` / `where node` first (respects user's PATH / shims)
const fromPath = whichNode();
if (fromPath && isRealNode(fromPath)) {
return { nodePath: fromPath };
}
// 2. Try nvm (has multiple version dirs)
const fromNvm = findNvmNode();
if (fromNvm && isRealNode(fromNvm)) {
return { nodePath: fromNvm };
}
// 3. Walk the well-known paths for the current platform
const platform = process.platform;
const candidates = KNOWN_PATHS[platform] || KNOWN_PATHS.linux;
for (const candidate of candidates) {
// Skip the nvm root — already handled above
if (candidate.includes(".nvm/versions/node")) continue;
if (fs.existsSync(candidate) && isRealNode(candidate)) {
return { nodePath: candidate };
}
}
return null;
}
/** Cache so we only search once per process lifetime. */
let _cached = undefined;
/**
* Cached version of `findSystemNode()`.
* @returns {{ nodePath: string } | null}
*/
function getSystemNode() {
if (_cached === undefined) {
_cached = findSystemNode();
if (_cached) {
console.log("[nodeDetect] Found system Node.js:", _cached.nodePath);
} else {
console.warn(
"[nodeDetect] No system Node.js found — will fall back to WASM backend",
);
}
}
return _cached;
}
module.exports = { findSystemNode, getSystemNode, isRealNode };
+5
View File
@@ -1087,6 +1087,11 @@ const theme = {
this.current = themeName; this.current = themeName;
const root = document.documentElement; const root = document.documentElement;
// Determine if theme is light or dark
const lightThemes = ["light", "sepia"];
const isLightTheme = lightThemes.includes(themeName);
document.body.setAttribute("data-theme-type", isLightTheme ? "light" : "dark");
// New design tokens (used by components) // New design tokens (used by components)
root.style.setProperty("--text-primary", colors.text); root.style.setProperty("--text-primary", colors.text);
root.style.setProperty("--text-secondary", colors.textSecondary); root.style.setProperty("--text-secondary", colors.textSecondary);
+332
View File
@@ -0,0 +1,332 @@
/**
* Whisper Worker runs ONNX Runtime in an isolated child process.
*
* The main Electron process forks this file and communicates via IPC messages.
* If ONNX Runtime crashes (SIGSEGV/SIGABRT inside the native Metal or CPU
* execution provider), only this worker dies the main process survives and
* can respawn the worker automatically.
*
* Protocol (parent worker):
* parent worker:
* { type: 'load', modelName, cacheDir, device? }
* { type: 'transcribe', audioBase64, language? } // PCM 16-bit 16kHz as base64
* { type: 'shutdown' }
*
* worker parent:
* { type: 'load-result', success, error?, device? }
* { type: 'transcribe-result', success, text?, error? }
* { type: 'status', message }
* { type: 'ready' }
*/
// ── Crash handlers — report fatal errors before the process dies ──
process.on("uncaughtException", (err) => {
try {
send({
type: "status",
message: `[Worker] Uncaught exception: ${err.message || err}`,
});
console.error("[WhisperWorker] Uncaught exception:", err);
} catch (_) {
// Cannot communicate with parent anymore
}
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
try {
send({
type: "status",
message: `[Worker] Unhandled rejection: ${reason?.message || reason}`,
});
console.error("[WhisperWorker] Unhandled rejection:", reason);
} catch (_) {
// Cannot communicate with parent anymore
}
// Don't exit — let it be caught by the pipeline's own handlers
});
let whisperPipeline = null;
/** Which ONNX backend is actually active: "cpu" | "wasm" */
let activeDevice = null;
function pcm16ToFloat32(pcm16Buffer) {
if (!pcm16Buffer || pcm16Buffer.length === 0) {
return new Float32Array(0);
}
const alignedLength =
pcm16Buffer.length % 2 === 0 ? pcm16Buffer.length : pcm16Buffer.length - 1;
const samples = alignedLength / 2;
const float32 = new Float32Array(samples);
for (let i = 0; i < samples; i++) {
float32[i] = pcm16Buffer.readInt16LE(i * 2) / 32768;
}
return float32;
}
/**
* Load the Whisper model.
*
* @param {string} modelName HuggingFace model id, e.g. "Xenova/whisper-small"
* @param {string} cacheDir Directory for cached model files
* @param {string} [device] "cpu" (onnxruntime-node) or "wasm" (onnxruntime-web).
* When "cpu" is requested we try native first and fall
* back to "wasm" on failure (ABI mismatch, etc.).
*/
async function loadModel(modelName, cacheDir, device = "cpu") {
if (whisperPipeline) {
send({ type: "load-result", success: true, device: activeDevice });
return;
}
try {
send({
type: "status",
message: "Loading Whisper model (first time may take a while)...",
});
// Validate / create cache directory
const fs = require("fs");
const path = require("path");
if (cacheDir) {
try {
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
console.log("[WhisperWorker] Created cache directory:", cacheDir);
}
} catch (mkdirErr) {
console.warn(
"[WhisperWorker] Cannot create cache dir:",
mkdirErr.message,
);
}
// Check for corrupted partial downloads — if an onnx file exists but
// is suspiciously small (< 1 KB), delete it so the library re-downloads.
try {
const modelDir = path.join(cacheDir, modelName.replace("/", path.sep));
if (fs.existsSync(modelDir)) {
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (
entry.name.endsWith(".onnx") &&
fs.statSync(full).size < 1024
) {
console.warn(
"[WhisperWorker] Removing likely-corrupt file:",
full,
);
fs.unlinkSync(full);
}
}
};
walk(modelDir);
}
} catch (cleanErr) {
console.warn("[WhisperWorker] Cache cleanup error:", cleanErr.message);
}
}
const { pipeline, env } = await import("@huggingface/transformers");
env.cacheDir = cacheDir;
// Attempt to load with the requested device
const devicesToTry = device === "wasm" ? ["wasm"] : ["cpu", "wasm"];
let lastError = null;
for (const dev of devicesToTry) {
try {
send({
type: "status",
message: `Loading Whisper (${dev} backend)...`,
});
console.log(
`[WhisperWorker] Trying device: ${dev}, model: ${modelName}`,
);
whisperPipeline = await pipeline(
"automatic-speech-recognition",
modelName,
{
dtype: "q8",
device: dev,
progress_callback: (progress) => {
// progress: { status, name?, file?, progress?, loaded?, total? }
if (
progress.status === "download" ||
progress.status === "progress"
) {
send({
type: "progress",
file: progress.file || progress.name || "",
progress: progress.progress ?? 0,
loaded: progress.loaded ?? 0,
total: progress.total ?? 0,
status: progress.status,
});
} else if (progress.status === "done") {
send({
type: "progress",
file: progress.file || progress.name || "",
progress: 100,
loaded: progress.total ?? 0,
total: progress.total ?? 0,
status: "done",
});
} else if (progress.status === "initiate") {
send({
type: "progress",
file: progress.file || progress.name || "",
progress: 0,
loaded: 0,
total: 0,
status: "initiate",
});
}
},
},
);
activeDevice = dev;
console.log(
`[WhisperWorker] Model loaded successfully (device: ${dev})`,
);
send({ type: "load-result", success: true, device: dev });
return;
} catch (err) {
lastError = err;
console.error(
`[WhisperWorker] Failed to load with device "${dev}":`,
err.message || err,
);
if (dev === "cpu" && devicesToTry.includes("wasm")) {
send({
type: "status",
message: `Native CPU backend failed (${err.message}). Trying WASM fallback...`,
});
}
// Reset pipeline state before retry
whisperPipeline = null;
}
}
// All devices failed
throw lastError || new Error("All ONNX backends failed");
} catch (error) {
send({ type: "load-result", success: false, error: error.message });
}
}
async function transcribe(audioBase64, language) {
if (!whisperPipeline) {
send({
type: "transcribe-result",
success: false,
error: "Whisper pipeline not loaded",
});
return;
}
try {
const pcm16Buffer = Buffer.from(audioBase64, "base64");
if (pcm16Buffer.length < 2) {
send({
type: "transcribe-result",
success: false,
error: "Audio buffer too small",
});
return;
}
// Cap at ~30 seconds (16kHz, 16-bit mono)
const maxBytes = 16000 * 2 * 30;
const audioData =
pcm16Buffer.length > maxBytes
? pcm16Buffer.slice(0, maxBytes)
: pcm16Buffer;
const float32Audio = pcm16ToFloat32(audioData);
if (float32Audio.length === 0) {
send({
type: "transcribe-result",
success: false,
error: "Empty audio after conversion",
});
return;
}
// Build pipeline options with the requested language
const pipelineOpts = {
sampling_rate: 16000,
task: "transcribe",
};
if (language && language !== "auto") {
pipelineOpts.language = language;
}
const result = await whisperPipeline(float32Audio, pipelineOpts);
const text = result.text?.trim() || "";
send({ type: "transcribe-result", success: true, text });
} catch (error) {
send({
type: "transcribe-result",
success: false,
error: error.message || String(error),
});
}
}
function send(msg) {
try {
if (process.send) {
process.send(msg);
}
} catch (_) {
// Parent may have disconnected
}
}
process.on("message", (msg) => {
switch (msg.type) {
case "load":
loadModel(msg.modelName, msg.cacheDir, msg.device).catch((err) => {
send({ type: "load-result", success: false, error: err.message });
});
break;
case "transcribe":
transcribe(msg.audioBase64, msg.language).catch((err) => {
send({ type: "transcribe-result", success: false, error: err.message });
});
break;
case "shutdown":
// Dispose the ONNX session gracefully before exiting to avoid
// native cleanup race conditions (SIGABRT on mutex destroy).
(async () => {
if (whisperPipeline) {
try {
if (typeof whisperPipeline.dispose === "function") {
await whisperPipeline.dispose();
}
} catch (_) {
// Best-effort cleanup
}
whisperPipeline = null;
}
// Small delay to let native threads wind down
setTimeout(() => process.exit(0), 200);
})();
break;
}
});
// Signal readiness to parent
send({ type: "ready" });