Squashed commit of the following:
commit d6c92ea0ad95c0b640ac9c7df48197412c7518e3
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Thu Aug 21 23:27:44 2025 +0530
fix: unpacking amical/smart-whisper dep
commit 87819819bb12c07b94f5b52cbb0ea42a452c16e2
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Thu Aug 21 17:41:02 2025 +0530
fix: unpacking of smart-whisper
commit 81cec166834606cbff2cdd2e750dcc1fb769d4f3
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Thu Aug 21 16:08:39 2025 +0530
chore: re-enable mac builds
commit f13069c1f350fe06c69aa8f16af41f983f34131e
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Thu Aug 21 13:06:26 2025 +0530
feat: add smart-whisper package with updated build configuration
commit a24e06856cc595f5e6c5d914090531716d208d2a
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Thu Aug 21 11:37:25 2025 +0530
chore: bump smart-whisper ver
commit 98f84b6f89c873370f1bb356f11c97dab0185ab7
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Wed Aug 20 08:59:55 2025 +0530
feat: release wf updates for win builds
commit a85825d362f2a27fdef7b49533a9139aea4785b7
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date: Wed Aug 20 08:36:13 2025 +0530
feat: add windows support basics
This commit is contained in:
parent
2d852a0d14
commit
17d034be80
59 changed files with 10524 additions and 3079 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 34 KiB |
|
|
@ -17,6 +17,8 @@ import {
|
|||
mkdirSync,
|
||||
cpSync,
|
||||
rmSync,
|
||||
lstatSync,
|
||||
readlinkSync,
|
||||
} from "node:fs";
|
||||
import { join, normalize } from "node:path";
|
||||
// Use flora-colossus for finding all dependencies of EXTERNAL_DEPENDENCIES
|
||||
|
|
@ -29,7 +31,6 @@ let nativeModuleDependenciesToPackage: string[] = [];
|
|||
|
||||
export const EXTERNAL_DEPENDENCIES = [
|
||||
"electron-squirrel-startup",
|
||||
"smart-whisper",
|
||||
"@libsql/client",
|
||||
"@libsql/darwin-arm64",
|
||||
"@libsql/darwin-x64",
|
||||
|
|
@ -39,13 +40,13 @@ export const EXTERNAL_DEPENDENCIES = [
|
|||
"libsql",
|
||||
"onnxruntime-node",
|
||||
"workerpool",
|
||||
"@amical/smart-whisper",
|
||||
// Add any other native modules you need here
|
||||
];
|
||||
|
||||
const config: ForgeConfig = {
|
||||
hooks: {
|
||||
prePackage: async (_forgeConfig, platform, arch) => {
|
||||
console.error("prePackage", { platform, arch });
|
||||
const projectRoot = normalize(__dirname);
|
||||
// In a monorepo, node_modules are typically at the root level
|
||||
const monorepoRoot = join(projectRoot, "../../"); // Go up to monorepo root
|
||||
|
|
@ -148,15 +149,56 @@ const config: ForgeConfig = {
|
|||
|
||||
// Copy the package
|
||||
console.log(`Copying ${dep}...`);
|
||||
cpSync(rootDepPath, localDepPath, { recursive: true });
|
||||
cpSync(rootDepPath, localDepPath, { recursive: true, dereference: true, force: true });
|
||||
console.log(`✓ Successfully copied ${dep}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to copy ${dep}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Replace any symlinks with dereferenced copies
|
||||
console.log("Checking for symlinks in copied dependencies...");
|
||||
for (const dep of nativeModuleDependenciesToPackage) {
|
||||
const localDepPath = join(localNodeModules, dep);
|
||||
|
||||
try {
|
||||
if (existsSync(localDepPath)) {
|
||||
const stats = lstatSync(localDepPath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
console.log(`Found symlink for ${dep}, replacing with dereferenced copy...`);
|
||||
|
||||
// Read where the symlink points to
|
||||
const symlinkTarget = readlinkSync(localDepPath);
|
||||
const absoluteTarget = join(localDepPath, "..", symlinkTarget);
|
||||
const sourcePath = normalize(absoluteTarget);
|
||||
|
||||
console.log(` Symlink points to: ${sourcePath}`);
|
||||
|
||||
// Remove the symlink
|
||||
rmSync(localDepPath, { recursive: true, force: true });
|
||||
|
||||
// Copy with dereference to get actual content
|
||||
cpSync(sourcePath, localDepPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
dereference: true // Follow symlinks and copy actual content
|
||||
});
|
||||
|
||||
console.log(`✓ Successfully replaced symlink for ${dep} with actual content`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check/replace symlink for ${dep}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune onnxruntime-node to keep only the required binary
|
||||
console.log("Pruning onnxruntime-node binaries...");
|
||||
const targetPlatform = platform;
|
||||
const targetArch = arch;
|
||||
|
||||
console.log(
|
||||
`Pruning onnxruntime-node binaries for ${targetPlatform}/${targetArch}...`,
|
||||
);
|
||||
const onnxBinRoot = join(localNodeModules, "onnxruntime-node", "bin");
|
||||
if (existsSync(onnxBinRoot)) {
|
||||
const napiVersionDirs = readdirSync(onnxBinRoot);
|
||||
|
|
@ -169,18 +211,18 @@ const config: ForgeConfig = {
|
|||
const platformPath = join(napiVersionPath, platformDir);
|
||||
if (!statSync(platformPath).isDirectory()) continue;
|
||||
|
||||
// Delete other platform directories
|
||||
if (platformDir !== process.platform) {
|
||||
// Delete unused platforms except Linux (keep for compatibility)
|
||||
if (platformDir !== targetPlatform && platformDir !== "linux") {
|
||||
console.log(`- Deleting unused platform: ${platformPath}`);
|
||||
rmSync(platformPath, { recursive: true, force: true });
|
||||
} else {
|
||||
} else if (platformDir === targetPlatform) {
|
||||
// Now in the correct platform dir, prune architectures
|
||||
const archDirs = readdirSync(platformPath);
|
||||
for (const archDir of archDirs) {
|
||||
const archPath = join(platformPath, archDir);
|
||||
if (!statSync(archPath).isDirectory()) continue;
|
||||
|
||||
if (archDir !== process.arch) {
|
||||
if (archDir !== targetArch) {
|
||||
console.log(`- Deleting unused arch: ${archPath}`);
|
||||
rmSync(archPath, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -196,6 +238,7 @@ const config: ForgeConfig = {
|
|||
}
|
||||
},
|
||||
packageAfterPrune: async (_forgeConfig, buildPath) => {
|
||||
console.error("PRE PACKAGE");
|
||||
try {
|
||||
function getItemsFromFolder(
|
||||
path: string,
|
||||
|
|
@ -264,14 +307,14 @@ const config: ForgeConfig = {
|
|||
packagerConfig: {
|
||||
asar: {
|
||||
unpack:
|
||||
"{*.node,*.dylib,*.so,*.dll,*.metal,**/whisper.cpp/**,**/.vite/build/whisper-worker-fork.js,**/node_modules/smart-whisper/**,**/node_modules/jest-worker/**}",
|
||||
"{*.node,*.dylib,*.so,*.dll,*.metal,**/node_modules/@amical/smart-whisper/**,**/whisper.cpp/**,**/.vite/build/whisper-worker-fork.js,**/node_modules/jest-worker/**,**/onnxruntime-node/bin/**}",
|
||||
},
|
||||
name: "Amical",
|
||||
executableName: "Amical",
|
||||
icon: "./assets/logo", // Path to your icon file
|
||||
appBundleId: "com.amical.desktop", // Proper bundle ID
|
||||
extraResource: [
|
||||
"../../packages/native-helpers/swift-helper/bin",
|
||||
`${process.platform === "win32" ? "../../packages/native-helpers/windows-helper/bin" : "../../packages/native-helpers/swift-helper/bin"}`,
|
||||
"./src/db/migrations",
|
||||
// Only include the platform-specific node binary
|
||||
`./node-binaries/${process.platform}-${process.arch}/node${
|
||||
|
|
@ -356,8 +399,18 @@ const config: ForgeConfig = {
|
|||
}
|
||||
|
||||
// Handle scoped packages: if dep is @scope/package, also keep @scope/ directory
|
||||
// But not for our workspace packages
|
||||
if (dep.includes("/") && dep.startsWith("@")) {
|
||||
const scopeDir = dep.split("/")[0]; // @libsql/client -> @libsql
|
||||
// for workspace packages only keep the actual package
|
||||
if (scopeDir === "@amical") {
|
||||
if (filePath.startsWith(`/node_modules/${dep}`) ||
|
||||
filePath === `/node_modules/${scopeDir}`) {
|
||||
KEEP_FILE.keep = true;
|
||||
KEEP_FILE.log = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
filePath === `/node_modules/${scopeDir}/` ||
|
||||
filePath === `/node_modules/${scopeDir}` ||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@amical/desktop",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.9-windows-ci-test",
|
||||
"description": "Amical Desktop app",
|
||||
"main": ".vite/build/main.js",
|
||||
"productName": "Amical",
|
||||
|
|
@ -8,6 +8,11 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/amicalhq/amical"
|
||||
},
|
||||
"author": {
|
||||
"name": "Amical",
|
||||
"email": "contact@amical.ai",
|
||||
"url": "https://amical.ai"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "pnpm build:deps && electron-forge start",
|
||||
"start:onboarding": "FORCE_ONBOARDING=true pnpm start",
|
||||
|
|
@ -22,6 +27,8 @@
|
|||
"make:zip:x64": "pnpm build:deps && electron-forge make --targets=@electron-forge/maker-zip --platform=darwin --arch=x64",
|
||||
"package:arm64": "pnpm build:deps && electron-forge package --platform=darwin --arch=arm64",
|
||||
"package:x64": "pnpm build:deps && electron-forge package --platform=darwin --arch=x64",
|
||||
"package:windows": "pnpm build:deps && electron-forge package --platform=win32 --arch=x64",
|
||||
"make:windows": "pnpm build:deps && electron-forge make --platform=win32 --arch=x64",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "eslint --ext .ts,.tsx .",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,md,json,mjs,mts,css,mdx}\" --cache --ignore-path=../../.prettierignore",
|
||||
|
|
@ -29,9 +36,11 @@
|
|||
"db:generate": "drizzle-kit generate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"build:deps": "pnpm build:types && pnpm build:swift-helper",
|
||||
"build:deps": "pnpm build:types && pnpm build:native-helper",
|
||||
"build:types": "pnpm --filter @amical/types build",
|
||||
"build:swift-helper": "pnpm --filter @amical/swift-helper build",
|
||||
"build:windows-helper": "pnpm --filter @amical/windows-helper build",
|
||||
"build:native-helper": "node -p \"process.platform === 'darwin' ? 'build:swift-helper' : process.platform === 'win32' ? 'build:windows-helper' : 'echo No native helpers'\" | xargs pnpm run",
|
||||
"dev": "pnpm start",
|
||||
"download-node": "tsx scripts/download-node-binaries.ts",
|
||||
"download-node:all": "tsx scripts/download-node-binaries.ts --all"
|
||||
|
|
@ -39,16 +48,16 @@
|
|||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.1",
|
||||
"@electron-forge/maker-deb": "^7.8.1",
|
||||
"@electron-forge/maker-dmg": "^7.8.1",
|
||||
"@electron-forge/maker-rpm": "^7.8.1",
|
||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
||||
"@electron-forge/maker-zip": "^7.8.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
||||
"@electron-forge/plugin-vite": "^7.8.1",
|
||||
"@electron-forge/publisher-github": "^7.8.1",
|
||||
"@electron-forge/cli": "7.8.2",
|
||||
"@electron-forge/maker-deb": "7.8.2",
|
||||
"@electron-forge/maker-dmg": "7.8.2",
|
||||
"@electron-forge/maker-rpm": "7.8.2",
|
||||
"@electron-forge/maker-squirrel": "7.8.2",
|
||||
"@electron-forge/maker-zip": "7.8.2",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.8.2",
|
||||
"@electron-forge/plugin-fuses": "7.8.2",
|
||||
"@electron-forge/plugin-vite": "7.8.2",
|
||||
"@electron-forge/publisher-github": "7.8.2",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.6",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
|
|
@ -78,6 +87,7 @@
|
|||
"@hookform/resolvers": "^5.0.1",
|
||||
"@libsql/client": "^0.15.9",
|
||||
"@libsql/darwin-x64": "0.5.13",
|
||||
"@libsql/win32-x64-msvc": "^0.5.13",
|
||||
"@openrouter/ai-sdk-provider": "^0.7.2",
|
||||
"@radix-ui/react-accordion": "^1.2.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||
|
|
@ -142,7 +152,7 @@
|
|||
"react-hook-form": "^7.56.3",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"recharts": "^2.15.3",
|
||||
"smart-whisper": "0.2.0",
|
||||
"@amical/smart-whisper": "workspace:*",
|
||||
"sonner": "^2.0.3",
|
||||
"split2": "^4.2.0",
|
||||
"superjson": "^2.2.2",
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ export class AppManager {
|
|||
return this.serviceManager.getService("transcriptionService");
|
||||
}
|
||||
|
||||
getSwiftIOBridge() {
|
||||
return this.serviceManager.getService("swiftIOBridge");
|
||||
getNativeBridge() {
|
||||
return this.serviceManager.getService("nativeBridge");
|
||||
}
|
||||
|
||||
getAutoUpdaterService() {
|
||||
|
|
|
|||
|
|
@ -11,23 +11,23 @@ export class EventHandlers {
|
|||
}
|
||||
|
||||
setupEventHandlers(): void {
|
||||
this.setupSwiftBridgeEventHandlers();
|
||||
this.setupNativeBridgeEventHandlers();
|
||||
this.setupGeneralIPCHandlers();
|
||||
this.setupOnboardingIPCHandlers();
|
||||
// Note: Audio IPC handlers are now managed by RecordingService
|
||||
}
|
||||
|
||||
private setupSwiftBridgeEventHandlers(): void {
|
||||
private setupNativeBridgeEventHandlers(): void {
|
||||
try {
|
||||
const swiftBridge = this.appManager.getSwiftIOBridge();
|
||||
if (!swiftBridge) {
|
||||
logger.main.warn("Swift bridge not available for event handlers");
|
||||
const nativeBridge = this.appManager.getNativeBridge();
|
||||
if (!nativeBridge) {
|
||||
logger.main.warn("Native bridge not available for event handlers");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle non-shortcut related events only
|
||||
swiftBridge.on("helperEvent", (event: HelperEvent) => {
|
||||
logger.swift.debug("Received helperEvent from SwiftIOBridge", {
|
||||
nativeBridge.on("helperEvent", (event: HelperEvent) => {
|
||||
logger.swift.debug("Received helperEvent from native bridge", {
|
||||
event,
|
||||
});
|
||||
|
||||
|
|
@ -35,15 +35,15 @@ export class EventHandlers {
|
|||
// This handler can process other helper events if needed
|
||||
});
|
||||
|
||||
swiftBridge.on("error", (error: Error) => {
|
||||
logger.main.error("SwiftIOBridge error:", error);
|
||||
nativeBridge.on("error", (error: Error) => {
|
||||
logger.main.error("Native bridge error:", error);
|
||||
});
|
||||
|
||||
swiftBridge.on("close", (code: number | null) => {
|
||||
logger.swift.warn("Swift helper process closed", { code });
|
||||
nativeBridge.on("close", (code: number | null) => {
|
||||
logger.swift.warn("Native helper process closed", { code });
|
||||
});
|
||||
} catch (error) {
|
||||
logger.main.warn("Swift bridge not available for event handlers");
|
||||
logger.main.warn("Native bridge not available for event handlers");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { app } from "electron";
|
|||
import * as path from "path";
|
||||
|
||||
// Set GGML_METAL_PATH_RESOURCES before any other imports
|
||||
// This ensures smart-whisper can find its resources when unpacked from asar
|
||||
// This ensures @amical/smart-whisper can find its resources when unpacked from asar
|
||||
if (app.isPackaged) {
|
||||
// Point to the unpacked whisper.cpp directory
|
||||
process.env.GGML_METAL_PATH_RESOURCES = path.join(
|
||||
process.resourcesPath,
|
||||
"app.asar.unpacked",
|
||||
"node_modules",
|
||||
"@amical",
|
||||
"smart-whisper",
|
||||
"whisper.cpp",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -218,12 +218,12 @@ export class RecordingManager extends EventEmitter {
|
|||
|
||||
// Mute system audio
|
||||
try {
|
||||
const swiftBridge = this.serviceManager.getService("swiftIOBridge");
|
||||
if (swiftBridge) {
|
||||
await swiftBridge.call("muteSystemAudio", {});
|
||||
const nativeBridge = this.serviceManager.getService("nativeBridge");
|
||||
if (nativeBridge) {
|
||||
await nativeBridge.call("muteSystemAudio", {});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.main.warn("Swift bridge not available for audio muting");
|
||||
logger.main.warn("Native bridge not available for audio muting");
|
||||
}
|
||||
|
||||
this.setState("recording");
|
||||
|
|
@ -252,12 +252,12 @@ export class RecordingManager extends EventEmitter {
|
|||
|
||||
// Restore system audio
|
||||
try {
|
||||
const swiftBridge = this.serviceManager.getService("swiftIOBridge");
|
||||
if (swiftBridge) {
|
||||
await swiftBridge.call("restoreSystemAudio", {});
|
||||
const nativeBridge = this.serviceManager.getService("nativeBridge");
|
||||
if (nativeBridge) {
|
||||
await nativeBridge.call("restoreSystemAudio", {});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.main.warn("Swift bridge not available for audio restore");
|
||||
logger.main.warn("Native bridge not available for audio restore");
|
||||
}
|
||||
|
||||
logger.audio.info("Recording stop initiated", {
|
||||
|
|
@ -412,14 +412,14 @@ export class RecordingManager extends EventEmitter {
|
|||
}
|
||||
|
||||
try {
|
||||
const swiftBridge = this.serviceManager.getService("swiftIOBridge");
|
||||
const nativeBridge = this.serviceManager.getService("nativeBridge");
|
||||
|
||||
logger.main.info("Pasting transcription to active application", {
|
||||
textLength: transcription.length,
|
||||
});
|
||||
|
||||
if (swiftBridge) {
|
||||
swiftBridge.call("pasteText", {
|
||||
if (nativeBridge) {
|
||||
nativeBridge.call("pasteText", {
|
||||
transcript: transcription,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { logger } from "../logger";
|
|||
import { ModelManagerService } from "../../services/model-manager";
|
||||
import { TranscriptionService } from "../../services/transcription-service";
|
||||
import { SettingsService } from "../../services/settings-service";
|
||||
import { SwiftIOBridge } from "../../services/platform/swift-bridge-service";
|
||||
import { NativeBridge } from "../../services/platform/native-bridge-service";
|
||||
import { AutoUpdaterService } from "../services/auto-updater";
|
||||
import { RecordingManager } from "./recording-manager";
|
||||
import { VADService } from "../../services/vad-service";
|
||||
|
|
@ -11,6 +11,7 @@ import { WindowManager } from "../core/window-manager";
|
|||
import { createIPCHandler } from "electron-trpc-experimental/main";
|
||||
import { router } from "../../trpc/router";
|
||||
import { createContext } from "../../trpc/context";
|
||||
import { isMacOS, isWindows } from "../../utils/platform";
|
||||
|
||||
/**
|
||||
* Service map for type-safe service access
|
||||
|
|
@ -20,7 +21,7 @@ export interface ServiceMap {
|
|||
transcriptionService: TranscriptionService;
|
||||
settingsService: SettingsService;
|
||||
vadService: VADService;
|
||||
swiftIOBridge: SwiftIOBridge;
|
||||
nativeBridge: NativeBridge;
|
||||
autoUpdaterService: AutoUpdaterService;
|
||||
recordingManager: RecordingManager;
|
||||
shortcutManager: ShortcutManager;
|
||||
|
|
@ -39,7 +40,7 @@ export class ServiceManager {
|
|||
private settingsService: SettingsService | null = null;
|
||||
private vadService: VADService | null = null;
|
||||
|
||||
private swiftIOBridge: SwiftIOBridge | null = null;
|
||||
private nativeBridge: NativeBridge | null = null;
|
||||
private autoUpdaterService: AutoUpdaterService | null = null;
|
||||
private recordingManager: RecordingManager | null = null;
|
||||
private shortcutManager: ShortcutManager | null = null;
|
||||
|
|
@ -145,9 +146,9 @@ export class ServiceManager {
|
|||
}
|
||||
|
||||
private initializePlatformServices(): void {
|
||||
// Initialize Swift bridge for macOS integration
|
||||
if (process.platform === "darwin") {
|
||||
this.swiftIOBridge = new SwiftIOBridge();
|
||||
// Initialize platform-specific bridge
|
||||
if (isMacOS() || isWindows()) {
|
||||
this.nativeBridge = new NativeBridge();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ export class ServiceManager {
|
|||
);
|
||||
}
|
||||
this.shortcutManager = new ShortcutManager(this.settingsService);
|
||||
await this.shortcutManager.initialize(this.swiftIOBridge);
|
||||
await this.shortcutManager.initialize(this.nativeBridge);
|
||||
|
||||
// Connect shortcut events to recording manager
|
||||
this.recordingManager.setupShortcutListeners(this.shortcutManager);
|
||||
|
|
@ -213,7 +214,7 @@ export class ServiceManager {
|
|||
transcriptionService: this.transcriptionService ?? undefined,
|
||||
settingsService: this.settingsService ?? undefined,
|
||||
vadService: this.vadService ?? undefined,
|
||||
swiftIOBridge: this.swiftIOBridge ?? undefined,
|
||||
nativeBridge: this.nativeBridge ?? undefined,
|
||||
autoUpdaterService: this.autoUpdaterService ?? undefined,
|
||||
recordingManager: this.recordingManager ?? undefined,
|
||||
shortcutManager: this.shortcutManager ?? undefined,
|
||||
|
|
@ -242,9 +243,9 @@ export class ServiceManager {
|
|||
await this.vadService.dispose();
|
||||
}
|
||||
|
||||
if (this.swiftIOBridge) {
|
||||
logger.main.info("Stopping Swift helper...");
|
||||
this.swiftIOBridge.stopHelper();
|
||||
if (this.nativeBridge) {
|
||||
logger.main.info("Stopping native helper...");
|
||||
this.nativeBridge.stopHelper();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { EventEmitter } from "events";
|
||||
import { globalShortcut } from "electron";
|
||||
import { SettingsService } from "@/services/settings-service";
|
||||
import { SwiftIOBridge } from "@/services/platform/swift-bridge-service";
|
||||
import { matchesShortcutKey, getKeyNameFromPayload } from "@/utils/keycode-map";
|
||||
import { NativeBridge } from "@/services/platform/native-bridge-service";
|
||||
import { getKeyNameFromPayload } from "@/utils/keycode-map";
|
||||
import { KeyEventPayload, HelperEvent } from "@amical/types";
|
||||
import { logger } from "@/main/logger";
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ export class ShortcutManager extends EventEmitter {
|
|||
toggleRecording: "",
|
||||
};
|
||||
private settingsService: SettingsService;
|
||||
private swiftIOBridge: SwiftIOBridge | null = null;
|
||||
private nativeBridge: NativeBridge | null = null;
|
||||
private isRecordingShortcut: boolean = false;
|
||||
|
||||
constructor(settingsService: SettingsService) {
|
||||
|
|
@ -33,8 +33,8 @@ export class ShortcutManager extends EventEmitter {
|
|||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
async initialize(swiftIOBridge: SwiftIOBridge | null) {
|
||||
this.swiftIOBridge = swiftIOBridge;
|
||||
async initialize(nativeBridge: NativeBridge | null) {
|
||||
this.nativeBridge = nativeBridge;
|
||||
await this.loadShortcuts();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
|
@ -59,12 +59,12 @@ export class ShortcutManager extends EventEmitter {
|
|||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
if (!this.swiftIOBridge) {
|
||||
log.warn("SwiftIOBridge not available, shortcuts will not work");
|
||||
if (!this.nativeBridge) {
|
||||
log.warn("Native bridge not available, shortcuts will not work");
|
||||
return;
|
||||
}
|
||||
|
||||
this.swiftIOBridge.on("helperEvent", (event: HelperEvent) => {
|
||||
this.nativeBridge.on("helperEvent", (event: HelperEvent) => {
|
||||
switch (event.type) {
|
||||
case "flagsChanged":
|
||||
this.handleFlagsChanged(event.payload);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Worker process entry point for fork
|
||||
import { Whisper } from "smart-whisper";
|
||||
import { Whisper } from "@amical/smart-whisper";
|
||||
|
||||
// Simple console-based logging for worker process
|
||||
const logger = {
|
||||
|
|
@ -29,7 +29,7 @@ const methods = {
|
|||
whisperInstance = null;
|
||||
}
|
||||
|
||||
const { Whisper } = await import("smart-whisper");
|
||||
const { Whisper } = await import("@amical/smart-whisper");
|
||||
whisperInstance = new Whisper(modelPath, { gpu: true });
|
||||
try {
|
||||
await whisperInstance.load();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// This file contains just the Whisper-specific operations that need to run in a separate process
|
||||
import { Whisper } from "smart-whisper";
|
||||
import { Whisper } from "@amical/smart-whisper";
|
||||
|
||||
// Simple console-based logging for worker process
|
||||
const logger = {
|
||||
|
|
@ -27,7 +27,7 @@ export async function initializeModel(modelPath: string): Promise<void> {
|
|||
whisperInstance = null;
|
||||
}
|
||||
|
||||
const { Whisper } = await import("smart-whisper");
|
||||
const { Whisper } = await import("@amical/smart-whisper");
|
||||
whisperInstance = new Whisper(modelPath, { gpu: true });
|
||||
try {
|
||||
await whisperInstance.load();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import process from "node:process"; // Added import for process
|
||||
import { app, app as electronApp } from "electron"; // electronApp for app.getAppPath() consistency
|
||||
import { app as electronApp } from "electron";
|
||||
import split2 from "split2";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { getNativeHelperName, getNativeHelperDir } from "../../utils/platform";
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { createScopedLogger } from "../../main/logger";
|
||||
|
|
@ -54,21 +54,21 @@ interface RPCMethods {
|
|||
}
|
||||
|
||||
// Define event types for the client
|
||||
interface SwiftIOBridgeEvents {
|
||||
interface NativeBridgeEvents {
|
||||
helperEvent: (event: HelperEvent) => void;
|
||||
error: (error: Error) => void;
|
||||
close: (code: number | null, signal: NodeJS.Signals | null) => void;
|
||||
ready: () => void; // Emitted when the helper process is successfully spawned
|
||||
}
|
||||
|
||||
export class SwiftIOBridge extends EventEmitter {
|
||||
export class NativeBridge extends EventEmitter {
|
||||
private proc: ChildProcessWithoutNullStreams | null = null;
|
||||
private pending = new Map<
|
||||
string,
|
||||
{ callback: (resp: RpcResponse) => void; startTime: number }
|
||||
>();
|
||||
private helperPath: string;
|
||||
private logger = createScopedLogger("swift-bridge");
|
||||
private logger = createScopedLogger("native-bridge");
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -77,7 +77,9 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
}
|
||||
|
||||
private determineHelperPath(): string {
|
||||
const helperName = "SwiftHelper"; // Swift native helper executable
|
||||
const helperName = getNativeHelperName();
|
||||
const helperDir = getNativeHelperDir();
|
||||
|
||||
return electronApp.isPackaged
|
||||
? path.join(process.resourcesPath, "bin", helperName)
|
||||
: path.join(
|
||||
|
|
@ -86,7 +88,7 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
"..",
|
||||
"packages",
|
||||
"native-helpers",
|
||||
"swift-helper",
|
||||
helperDir,
|
||||
"bin",
|
||||
helperName,
|
||||
);
|
||||
|
|
@ -96,20 +98,33 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
try {
|
||||
fs.accessSync(this.helperPath, fs.constants.X_OK);
|
||||
} catch (err) {
|
||||
this.logger.error("SwiftHelper executable not found or not executable", {
|
||||
helperPath: this.helperPath,
|
||||
});
|
||||
this.emit(
|
||||
"error",
|
||||
new Error(
|
||||
`Helper executable not found at ${this.helperPath}. Attempt to build it if in dev mode.`,
|
||||
),
|
||||
const helperName = getNativeHelperName();
|
||||
this.logger.error(
|
||||
`${helperName} executable not found or not executable`,
|
||||
{
|
||||
helperPath: this.helperPath,
|
||||
},
|
||||
);
|
||||
// In a real app, you might try to build it here or provide more robust error handling.
|
||||
// In production, provide a more user-friendly error message
|
||||
const errorMessage = electronApp.isPackaged
|
||||
? `${helperName} is not available. Some features may not work correctly.`
|
||||
: `Helper executable not found at ${this.helperPath}. Please build it first.`;
|
||||
|
||||
this.emit("error", new Error(errorMessage));
|
||||
|
||||
// Log detailed error for debugging
|
||||
this.logger.error("Helper initialization failed", {
|
||||
helperPath: this.helperPath,
|
||||
isPackaged: electronApp.isPackaged,
|
||||
platform: process.platform,
|
||||
error: err,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info("Spawning SwiftHelper", { helperPath: this.helperPath });
|
||||
const helperName = getNativeHelperName();
|
||||
this.logger.info(`Spawning ${helperName}`, { helperPath: this.helperPath });
|
||||
this.proc = spawn(this.helperPath, [], { stdio: ["pipe", "pipe", "pipe"] });
|
||||
|
||||
this.proc.stdout.pipe(split2()).on("data", (line: string) => {
|
||||
|
|
@ -150,19 +165,24 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
|
||||
this.proc.stderr.on("data", (data: Buffer) => {
|
||||
const errorMsg = data.toString();
|
||||
this.logger.warn("SwiftHelper stderr output", { message: errorMsg });
|
||||
const helperName = getNativeHelperName();
|
||||
this.logger.warn(`${helperName} stderr output`, { message: errorMsg });
|
||||
// Don't emit as error since stderr is often just debug info
|
||||
// this.emit('error', new Error(`Helper stderr: ${errorMsg}`));
|
||||
});
|
||||
|
||||
this.proc.on("error", (err) => {
|
||||
this.logger.error("Failed to start SwiftHelper process", { error: err });
|
||||
const helperName = getNativeHelperName();
|
||||
this.logger.error(`Failed to start ${helperName} process`, {
|
||||
error: err,
|
||||
});
|
||||
this.emit("error", err);
|
||||
this.proc = null;
|
||||
});
|
||||
|
||||
this.proc.on("close", (code, signal) => {
|
||||
this.logger.info("SwiftHelper process exited", { code, signal });
|
||||
const helperName = getNativeHelperName();
|
||||
this.logger.info(`${helperName} process exited`, { code, signal });
|
||||
this.emit("close", code, signal);
|
||||
this.proc = null;
|
||||
// Optionally, implement retry logic or notify further
|
||||
|
|
@ -180,11 +200,18 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
timeoutMs = 5000,
|
||||
): Promise<RPCMethods[M]["result"]> {
|
||||
if (!this.proc || !this.proc.stdin || !this.proc.stdin.writable) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Swift helper process is not running or stdin is not writable.",
|
||||
),
|
||||
);
|
||||
const helperName = getNativeHelperName();
|
||||
const errorMessage = electronApp.isPackaged
|
||||
? `${helperName} is not available for this operation.`
|
||||
: "Native helper process is not running or stdin is not writable.";
|
||||
|
||||
this.logger.warn(`Cannot call ${method}: helper not available`, {
|
||||
method,
|
||||
isPackaged: electronApp.isPackaged,
|
||||
platform: process.platform,
|
||||
});
|
||||
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
|
|
@ -267,7 +294,7 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
const duration = timedOutAt - startTime;
|
||||
reject(
|
||||
new Error(
|
||||
`SwiftIOBridge: RPC call "${method}" (id: ${id}) timed out after ${timeoutMs}ms (duration: ${duration}ms, started: ${new Date(startTime).toISOString()})`,
|
||||
`NativeBridge: RPC call "${method}" (id: ${id}) timed out after ${timeoutMs}ms (duration: ${duration}ms, started: ${new Date(startTime).toISOString()})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -283,24 +310,25 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
|
||||
public stopHelper(): void {
|
||||
if (this.proc) {
|
||||
this.logger.info("Stopping SwiftHelper process");
|
||||
const helperName = getNativeHelperName();
|
||||
this.logger.info(`Stopping ${helperName} process`);
|
||||
this.proc.kill();
|
||||
this.proc = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Typed event emitter methods
|
||||
on<E extends keyof SwiftIOBridgeEvents>(
|
||||
on<E extends keyof NativeBridgeEvents>(
|
||||
event: E,
|
||||
listener: SwiftIOBridgeEvents[E],
|
||||
listener: NativeBridgeEvents[E],
|
||||
): this {
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
emit<E extends keyof SwiftIOBridgeEvents>(
|
||||
emit<E extends keyof NativeBridgeEvents>(
|
||||
event: E,
|
||||
...args: Parameters<SwiftIOBridgeEvents[E]>
|
||||
...args: Parameters<NativeBridgeEvents[E]>
|
||||
): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
|
@ -10,12 +10,12 @@ class AppContextStore {
|
|||
const serviceManager = ServiceManager.getInstance();
|
||||
if (!serviceManager) return; // Silent fail
|
||||
|
||||
const swiftBridge = serviceManager.getService("swiftIOBridge");
|
||||
if (!swiftBridge) {
|
||||
logger.main.warn("SwiftIOBridge not available");
|
||||
const nativeBridge = serviceManager.getService("nativeBridge");
|
||||
if (!nativeBridge) {
|
||||
logger.main.warn("Native bridge not available");
|
||||
return;
|
||||
}
|
||||
const context = await swiftBridge.call("getAccessibilityContext", {
|
||||
const context = await nativeBridge.call("getAccessibilityContext", {
|
||||
editableOnly: false,
|
||||
});
|
||||
this.accessibilityContext = context;
|
||||
|
|
|
|||
53
apps/desktop/src/utils/platform.ts
Normal file
53
apps/desktop/src/utils/platform.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import process from "node:process";
|
||||
|
||||
/**
|
||||
* Platform detection utilities
|
||||
*/
|
||||
|
||||
export type Platform = "darwin" | "win32" | "linux";
|
||||
|
||||
export function getPlatform(): Platform {
|
||||
return process.platform as Platform;
|
||||
}
|
||||
|
||||
export function isWindows(): boolean {
|
||||
return process.platform === "win32";
|
||||
}
|
||||
|
||||
export function isMacOS(): boolean {
|
||||
return process.platform === "darwin";
|
||||
}
|
||||
|
||||
export function isLinux(): boolean {
|
||||
return process.platform === "linux";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the native helper name for the current platform
|
||||
*/
|
||||
export function getNativeHelperName(): string {
|
||||
return isWindows() ? "WindowsHelper.exe" : "SwiftHelper";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the native helper directory name for the current platform
|
||||
*/
|
||||
export function getNativeHelperDir(): string {
|
||||
return isWindows() ? "windows-helper" : "swift-helper";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a platform-specific display name
|
||||
*/
|
||||
export function getPlatformDisplayName(): string {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "macOS";
|
||||
case "win32":
|
||||
return "Windows";
|
||||
case "linux":
|
||||
return "Linux";
|
||||
default:
|
||||
return process.platform;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ export default defineConfig({
|
|||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [
|
||||
"smart-whisper",
|
||||
"@amical/smart-whisper",
|
||||
"@libsql/client",
|
||||
"@libsql/darwin-arm64",
|
||||
"@libsql/darwin-x64",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue