mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
- Add platform notes for tray startup, shortcut saving, and Linux-only settings - Handle stat error in processRecording (missing file) - Select correct binary name based on OS in updater - Increase tray icon canvas size from 64 to 128 pixels - Update frontend dist asset hash
755 lines
20 KiB
Go
755 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"wis-free-v3/internal/audio/recorder"
|
|
"wis-free-v3/internal/config"
|
|
"wis-free-v3/internal/hotkey"
|
|
"wis-free-v3/internal/logger"
|
|
"wis-free-v3/internal/platform"
|
|
"wis-free-v3/internal/services/transcriber"
|
|
"wis-free-v3/internal/services/whisper"
|
|
"wis-free-v3/internal/ui/tray"
|
|
|
|
"github.com/go-vgo/robotgo"
|
|
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// App struct
|
|
type App struct {
|
|
ctx context.Context
|
|
audioRecorder *recorder.AudioRecorder
|
|
hotkeyListener *hotkey.Listener
|
|
transcriber *transcriber.Client
|
|
config *config.Config
|
|
overlay platform.Overlay
|
|
recordingPath string
|
|
recording int32
|
|
isQuitting bool
|
|
wasMediaPlaying bool
|
|
whisperManager *whisper.Manager
|
|
tempDir string
|
|
}
|
|
|
|
// NewApp creates a new App application struct
|
|
func NewApp() *App {
|
|
return &App{}
|
|
}
|
|
|
|
// GetConfig returns the app configuration
|
|
func (a *App) GetConfig() *config.Config {
|
|
return a.config
|
|
}
|
|
|
|
// Version returns the application version (see scripts/VERSION and build scripts).
|
|
func (a *App) Version() string {
|
|
return AppVersion
|
|
}
|
|
|
|
// ShowSettings shows the settings window
|
|
func (a *App) ShowSettings() {
|
|
if a.ctx != nil {
|
|
wailsruntime.WindowShow(a.ctx)
|
|
}
|
|
}
|
|
|
|
// startup is called when the app starts
|
|
func (a *App) startup(ctx context.Context) {
|
|
a.ctx = ctx
|
|
|
|
// Create and clean private temp directory for recordings (Cleanup in background)
|
|
a.tempDir = filepath.Join(os.TempDir(), "wis-free-v3-recordings")
|
|
os.MkdirAll(a.tempDir, 0700)
|
|
|
|
go func() {
|
|
// Clean up any orphaned files from previous sessions
|
|
if files, err := os.ReadDir(a.tempDir); err == nil {
|
|
for _, f := range files {
|
|
os.Remove(filepath.Join(a.tempDir, f.Name()))
|
|
}
|
|
}
|
|
logger.Info("Orphaned temp files cleaned")
|
|
}()
|
|
|
|
title := appTitle
|
|
switch AppVersion {
|
|
case "", "dev":
|
|
title = appTitle + " (dev)"
|
|
default:
|
|
title = appTitle + " v" + AppVersion
|
|
}
|
|
wailsruntime.WindowSetTitle(ctx, title)
|
|
|
|
// Initialize components
|
|
a.startupHeadless()
|
|
a.startLinuxPressDaemon()
|
|
|
|
// When the user launches the app again while it is already running (tray-only),
|
|
// the second process signals us here so the settings window becomes visible.
|
|
go func() {
|
|
for range secondInstanceWake {
|
|
wailsruntime.WindowShow(ctx)
|
|
}
|
|
}()
|
|
|
|
// Accept explicit commands from helper invocations (Linux desktop shortcuts).
|
|
go func() {
|
|
for cmd := range secondInstanceCommand {
|
|
switch cmd {
|
|
case instanceCmdShow:
|
|
wailsruntime.WindowShow(ctx)
|
|
case instanceCmdStart:
|
|
a.StartRecording()
|
|
case instanceCmdStop:
|
|
a.StopRecording()
|
|
case instanceCmdToggle:
|
|
a.ToggleRecording()
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Register callback so the tray can notify the frontend when the startup
|
|
// menu item is toggled directly from the tray icon context menu.
|
|
tray.SetOnStartupChanged(func(enabled bool) {
|
|
if a.ctx != nil {
|
|
wailsruntime.EventsEmit(a.ctx, "startup:changed", enabled)
|
|
}
|
|
})
|
|
|
|
// Start system tray in a goroutine
|
|
// PLATFORM NOTE: On Linux, Wails' GTK main loop needs tray.Start() to be
|
|
// called synchronously (uses systray.Register). On Windows (and macOS),
|
|
// tray.Start() blocks for the Win32 message pump, so it must run in a goroutine.
|
|
if runtime.GOOS == "linux" {
|
|
tray.Start(a)
|
|
} else {
|
|
go tray.Start(a)
|
|
}
|
|
|
|
if initialAction != "" {
|
|
action := initialAction
|
|
initialAction = ""
|
|
go func() {
|
|
switch action {
|
|
case "show":
|
|
wailsruntime.WindowShow(ctx)
|
|
case "start":
|
|
a.StartRecording()
|
|
case "stop":
|
|
a.StopRecording()
|
|
case "toggle":
|
|
a.ToggleRecording()
|
|
default:
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// beforeClose is called when the window is about to close
|
|
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
|
if a.isQuitting {
|
|
return false
|
|
}
|
|
wailsruntime.WindowHide(ctx)
|
|
return true
|
|
}
|
|
|
|
// Quit handles application exit from tray
|
|
func (a *App) Quit() {
|
|
a.isQuitting = true
|
|
logger.Info("Application quitting...")
|
|
logger.Close()
|
|
wailsruntime.Quit(a.ctx)
|
|
}
|
|
|
|
var lastToggleUnixNano int64
|
|
|
|
func (a *App) ToggleRecording() {
|
|
// Debounce toggle calls to prevent rapid firing from double-binds or Wayland glitches
|
|
now := time.Now().UnixNano()
|
|
last := atomic.LoadInt64(&lastToggleUnixNano)
|
|
if last != 0 && time.Duration(now-last) < 250*time.Millisecond {
|
|
return
|
|
}
|
|
atomic.StoreInt64(&lastToggleUnixNano, now)
|
|
|
|
if atomic.LoadInt32(&a.recording) == 1 {
|
|
a.StopRecording()
|
|
return
|
|
}
|
|
a.StartRecording()
|
|
}
|
|
|
|
// StartRecording starts the audio recording
|
|
func (a *App) StartRecording() {
|
|
if !atomic.CompareAndSwapInt32(&a.recording, 0, 1) {
|
|
return
|
|
}
|
|
logger.Info("StartRecording triggered")
|
|
|
|
// 1. Show overlay immediately (most visible feedback)
|
|
if a.overlay != nil {
|
|
a.overlay.Show("Recording...")
|
|
}
|
|
|
|
tray.IncrementTriggerCount()
|
|
|
|
// 2. Start recorder as soon as possible
|
|
if a.audioRecorder == nil {
|
|
logger.Error("Recorder not initialized")
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
atomic.StoreInt32(&a.recording, 0)
|
|
return
|
|
}
|
|
|
|
// Prepare path safely in our private temp directory
|
|
tempFile, err := os.CreateTemp(a.tempDir, "rec_*.wav")
|
|
if err != nil {
|
|
logger.Error("Failed to create temp file: %v", err)
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
atomic.StoreInt32(&a.recording, 0)
|
|
return
|
|
}
|
|
a.recordingPath = tempFile.Name()
|
|
tempFile.Close() // We just need the path, recorder will open it
|
|
|
|
// Start recording
|
|
err = a.audioRecorder.Start(a.recordingPath)
|
|
if err != nil {
|
|
logger.Error("Failed to start recording: %v", err)
|
|
// IDIOT-PROOFING: Fallback to default
|
|
if a.config.MicrophoneDevice != nil {
|
|
a.audioRecorder.SetDevice("")
|
|
err = a.audioRecorder.Start(a.recordingPath)
|
|
}
|
|
if err != nil {
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
atomic.StoreInt32(&a.recording, 0)
|
|
return
|
|
}
|
|
}
|
|
|
|
// 3. Update tray status
|
|
tray.UpdateStatus("Recording...")
|
|
|
|
// Pause media if playing (do synchronously so wasMediaPlaying is ready before Stop)
|
|
a.wasMediaPlaying = platform.PauseMedia()
|
|
if a.wasMediaPlaying {
|
|
logger.Info("Media paused for recording")
|
|
}
|
|
}
|
|
|
|
// StopRecording stops the audio recording and triggers transcription
|
|
func (a *App) StopRecording() {
|
|
if !atomic.CompareAndSwapInt32(&a.recording, 1, 0) {
|
|
return
|
|
}
|
|
logger.Info("StopRecording triggered")
|
|
|
|
// Resume media if it was playing before
|
|
platform.ResumeMedia(a.wasMediaPlaying)
|
|
if a.wasMediaPlaying {
|
|
logger.Info("Media resumed after recording")
|
|
}
|
|
|
|
if a.audioRecorder == nil {
|
|
atomic.StoreInt32(&a.recording, 0)
|
|
return
|
|
}
|
|
|
|
err := a.audioRecorder.Stop()
|
|
if err != nil {
|
|
logger.Error("Failed to stop recording: %v", err)
|
|
return
|
|
}
|
|
|
|
// Capture the path before it can be overwritten by another immediate start
|
|
pathToProcess := a.recordingPath
|
|
// Transcribe in a goroutine to avoid blocking
|
|
go a.processRecording(pathToProcess)
|
|
}
|
|
|
|
// processRecording handles transcription and pasting
|
|
func (a *App) processRecording(recordingPath string) {
|
|
if recordingPath == "" {
|
|
logger.Error("No recording path set")
|
|
tray.UpdateStatus("Ready")
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
return
|
|
}
|
|
|
|
// IDIOT-PROOFING: Ignore extremely short recordings (less than ~100ms or ~3KB)
|
|
// that are likely accidental clicks or hardware glitches.
|
|
stat, statErr := os.Stat(recordingPath)
|
|
if statErr != nil {
|
|
logger.Error("Failed to stat recording file: %v", statErr)
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
tray.UpdateStatus("Ready")
|
|
return
|
|
}
|
|
if stat.Size() < 4000 {
|
|
logger.Info("Discarding tiny recording (%d bytes)", stat.Size())
|
|
os.Remove(recordingPath)
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
tray.UpdateStatus("Ready")
|
|
return
|
|
}
|
|
|
|
logger.Info("Transcribing audio (%d bytes)...", stat.Size())
|
|
tray.UpdateStatus("Transcribing...")
|
|
if a.overlay != nil {
|
|
a.overlay.Show("Transcribing...")
|
|
}
|
|
|
|
startTranscribe := time.Now()
|
|
var text string
|
|
var err error
|
|
|
|
// Check if using local whisper
|
|
isLocal := strings.HasPrefix(a.config.WhisperModel, "local-")
|
|
|
|
// IDIOT-PROOFING: Validate API key before attempting cloud transcription
|
|
if !isLocal && !strings.HasPrefix(a.config.APIKey, "gsk_") {
|
|
err = fmt.Errorf("invalid API key - must start with gsk_")
|
|
} else if isLocal {
|
|
// Use local whisper.cpp
|
|
if a.whisperManager == nil {
|
|
var mgrErr error
|
|
a.whisperManager, mgrErr = whisper.NewManager()
|
|
if mgrErr != nil {
|
|
logger.Error("Failed to create whisper manager: %v", mgrErr)
|
|
err = mgrErr
|
|
}
|
|
}
|
|
|
|
if a.whisperManager != nil {
|
|
text, err = a.whisperManager.Transcribe(recordingPath, a.config.Language)
|
|
}
|
|
} else {
|
|
// Use cloud API
|
|
text, err = a.transcriber.TranscribeAudio(recordingPath, a.config.Language)
|
|
}
|
|
transcribeDuration := time.Since(startTranscribe)
|
|
logger.Info("Transcription completed in %v", transcribeDuration)
|
|
|
|
if err != nil {
|
|
logger.Error("Transcription failed: %v", err)
|
|
tray.UpdateStatus("Ready")
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
return
|
|
}
|
|
|
|
logger.Info("Transcribed %d characters", len(text))
|
|
|
|
activeWindow := robotgo.GetTitle()
|
|
logger.Info("Active window for context: %s", activeWindow)
|
|
|
|
// Refine text (optional)
|
|
startRefine := time.Now()
|
|
refinedText, err := a.transcriber.RefineText(text, activeWindow)
|
|
if err != nil {
|
|
logger.Error("Refinement failed: %v", err)
|
|
// Fallback to original text
|
|
refinedText = text
|
|
} else {
|
|
refineDuration := time.Since(startRefine)
|
|
logger.Info("AI Refinement completed in %v (%d chars)", refineDuration, len(refinedText))
|
|
}
|
|
|
|
// Save to history
|
|
a.config.AddHistoryItem(refinedText, time.Now().Format(time.RFC3339))
|
|
|
|
if err := config.Save(a.config, ""); err != nil {
|
|
logger.Error("Failed to save config after history update: %v", err)
|
|
} else if a.ctx != nil {
|
|
wailsruntime.EventsEmit(a.ctx, "history:updated")
|
|
}
|
|
|
|
a.insertTranscription(refinedText)
|
|
|
|
// Clean up the recording file
|
|
if removeErr := os.Remove(recordingPath); removeErr != nil {
|
|
logger.Error("Failed to remove temporary recording file: %v", removeErr)
|
|
}
|
|
logger.Info("Processing complete!")
|
|
tray.UpdateStatus("Ready")
|
|
|
|
// Hide overlay after a short delay
|
|
time.Sleep(1 * time.Second)
|
|
if a.overlay != nil {
|
|
a.overlay.Hide()
|
|
}
|
|
}
|
|
|
|
// GetSettings returns the current configuration
|
|
func (a *App) GetSettings() map[string]interface{} {
|
|
conf := make(map[string]interface{})
|
|
conf["api_key"] = a.config.APIKey
|
|
conf["shortcut"] = a.config.Shortcut
|
|
conf["whisper_model"] = a.config.WhisperModel
|
|
conf["ai_model"] = a.config.AIModel
|
|
conf["ai_prompt"] = a.config.AIPrompt
|
|
conf["language"] = a.config.Language
|
|
conf["microphone_device"] = a.config.MicrophoneDevice
|
|
conf["history"] = a.config.History
|
|
conf["startup"] = platform.IsInStartup()
|
|
conf["app_version"] = AppVersion
|
|
// PLATFORM NOTE: Linux-only settings — press daemon command and ydotool status.
|
|
// These are not included in the Windows build. See linux_press_daemon.go
|
|
// and text_insert_linux.go for the implementations.
|
|
if runtime.GOOS == "linux" {
|
|
if exePath, err := os.Executable(); err == nil {
|
|
conf["linux_press_command"] = exePath + " --press"
|
|
}
|
|
conf["linux_press_mode"] = true
|
|
conf["linux_ydotool_status"] = linuxYdotoolStatus()
|
|
}
|
|
return conf
|
|
}
|
|
|
|
// SaveSettings updates the configuration
|
|
func (a *App) SaveSettings(settings map[string]interface{}) string {
|
|
if val, ok := settings["api_key"].(string); ok {
|
|
a.config.APIKey = val
|
|
}
|
|
// PLATFORM NOTE: Shortcut saving is disabled on Linux because Linux uses
|
|
// the `--press` daemon approach (GNOME custom shortcuts) instead of the
|
|
// built-in hotkey listener. On Windows, we allow the user to configure
|
|
// the shortcut through the settings UI.
|
|
if runtime.GOOS != "linux" {
|
|
if val, ok := settings["shortcut"].(string); ok {
|
|
_, _, modOnly, ok := hotkey.ParseShortcut(val)
|
|
if !ok {
|
|
logger.Error("Invalid shortcut: %s (rejected)", val)
|
|
return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)"
|
|
}
|
|
if modOnly && runtime.GOOS != "windows" {
|
|
return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows"
|
|
}
|
|
|
|
a.config.Shortcut = val
|
|
// Update existing listener with new shortcut (hot-swap)
|
|
if a.hotkeyListener != nil {
|
|
a.hotkeyListener.UpdateShortcut(val)
|
|
} else {
|
|
// Should not happen if app started correctly, but just in case
|
|
a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording)
|
|
if runtime.GOOS != "windows" {
|
|
a.hotkeyListener.SetRegistrationErrorCallback(func(err error) {
|
|
logger.Error("Linux hotkey registration failed: %v", err)
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
if a.overlay != nil {
|
|
a.overlay.Show("Shortcut registration failed. Please add a custom system shortcut calling 'wis-free-v3 --action=toggle' as a fallback.")
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
a.hotkeyListener.Start()
|
|
}
|
|
}
|
|
}
|
|
if val, ok := settings["whisper_model"].(string); ok {
|
|
a.config.WhisperModel = val
|
|
}
|
|
if val, ok := settings["ai_model"].(string); ok {
|
|
a.config.AIModel = val
|
|
}
|
|
if val, ok := settings["ai_prompt"].(string); ok {
|
|
a.config.AIPrompt = val
|
|
}
|
|
if val, ok := settings["language"].(string); ok {
|
|
a.config.Language = val
|
|
}
|
|
if val, ok := settings["microphone_device"]; ok {
|
|
if val == nil {
|
|
a.config.MicrophoneDevice = nil
|
|
} else {
|
|
// Handle float64 from JSON/JS
|
|
if f, ok := val.(float64); ok {
|
|
i := int(f)
|
|
a.config.MicrophoneDevice = &i
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save to file
|
|
if err := config.Save(a.config, ""); err != nil {
|
|
logger.Error("Failed to save config: %v", err)
|
|
return fmt.Sprintf("Error saving settings: %v", err)
|
|
}
|
|
|
|
// Re-init transcriber with new settings
|
|
a.transcriber = transcriber.NewClient(
|
|
a.config.APIKey,
|
|
a.config.WhisperModel,
|
|
a.config.AIModel,
|
|
a.config.AIPrompt,
|
|
)
|
|
|
|
logger.Info("Settings saved")
|
|
return "Settings saved successfully"
|
|
}
|
|
|
|
// GetMicrophones returns available microphone devices
|
|
func (a *App) GetMicrophones() []map[string]interface{} {
|
|
var result []map[string]interface{}
|
|
|
|
if a.audioRecorder != nil {
|
|
mics, err := a.audioRecorder.GetMicrophones()
|
|
if err != nil {
|
|
logger.Error("Failed to enumerate microphones: %v", err)
|
|
} else {
|
|
for i, mic := range mics {
|
|
result = append(result, map[string]interface{}{
|
|
"index": i - 1, // -1 for default, 0+ for specific devices
|
|
"name": mic.Name,
|
|
"id": mic.ID,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Fallback to just default
|
|
return []map[string]interface{}{
|
|
{"index": -1, "name": "System Default", "id": ""},
|
|
}
|
|
}
|
|
|
|
// ToggleStartup toggles Windows startup status
|
|
func (a *App) ToggleStartup(enable bool) string {
|
|
var err error
|
|
if enable {
|
|
err = platform.AddToStartup()
|
|
} else {
|
|
err = platform.RemoveFromStartup()
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Error("Startup toggle error: %v", err)
|
|
return fmt.Sprintf("Error: %v", err)
|
|
}
|
|
|
|
// Keep the tray menu item in sync when changed from the settings UI
|
|
tray.SetStartupChecked(enable)
|
|
|
|
return "Success"
|
|
}
|
|
|
|
// ClearHistory clears the transcription history
|
|
func (a *App) ClearHistory() {
|
|
a.config.History = []config.HistoryItem{}
|
|
config.Save(a.config, "")
|
|
logger.Info("History cleared")
|
|
}
|
|
|
|
// startupHeadless initializes the app without Wails context
|
|
func (a *App) startupHeadless() {
|
|
// Initialize Logger
|
|
err := logger.Init()
|
|
if err != nil {
|
|
fmt.Printf("Failed to initialize logger: %v\n", err)
|
|
}
|
|
|
|
// Load configuration
|
|
configPath, err := config.GetConfigPath()
|
|
if err != nil {
|
|
logger.Error("Failed to get config path: %v", err)
|
|
a.config = config.DefaultConfig()
|
|
} else {
|
|
a.config, err = config.Load(configPath)
|
|
if err != nil {
|
|
logger.Error("Failed to load config: %v", err)
|
|
a.config = config.DefaultConfig()
|
|
}
|
|
}
|
|
|
|
// Initialize heavy components synchronously instead of in background.
|
|
// This prevents a known ALSA/GTK initialization race condition bug on Linux
|
|
// where Miniaudio and GTK try to probe audio devices concurrently resulting in SIGABRT.
|
|
|
|
// Initialize transcriber
|
|
a.transcriber = transcriber.NewClient(
|
|
a.config.APIKey,
|
|
a.config.WhisperModel,
|
|
a.config.AIModel,
|
|
a.config.AIPrompt,
|
|
)
|
|
|
|
// Initialize whisper manager
|
|
a.whisperManager, _ = whisper.NewManager()
|
|
|
|
// Initialize Audio Recorder
|
|
rec, err := recorder.NewRecorder()
|
|
if err != nil {
|
|
logger.Error("Error initializing recorder: %v", err)
|
|
} else {
|
|
a.audioRecorder = rec
|
|
}
|
|
|
|
// Initialize Overlay
|
|
a.overlay = platform.NewOverlay()
|
|
|
|
// Connect volume feedback from recorder to overlay
|
|
if a.audioRecorder != nil && a.overlay != nil {
|
|
a.audioRecorder.OnVolume = func(level float64) {
|
|
a.overlay.SetVolume(level)
|
|
}
|
|
}
|
|
|
|
// Ensure desktop integration for Wayland portals
|
|
if err := platform.EnsureDesktopFile(tray.DefaultIconBytes()); err != nil {
|
|
logger.Error("Failed to ensure desktop file: %v", err)
|
|
}
|
|
|
|
// Initialize Hotkey Listener
|
|
if shouldStartBuiltInHotkeyListener() {
|
|
a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.StartRecording, a.StopRecording)
|
|
if runtime.GOOS != "windows" {
|
|
a.hotkeyListener.SetRegistrationErrorCallback(func(err error) {
|
|
logger.Error("Linux hotkey registration failed: %v", err)
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
if a.overlay != nil {
|
|
a.overlay.Show("Shortcut registration failed. Add a custom system shortcut with the command shown in Settings.")
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
a.hotkeyListener.Start()
|
|
} else {
|
|
logger.Info("Linux portal hotkey disabled; use the --press command from Settings for GNOME shortcuts")
|
|
}
|
|
|
|
logger.Info("Components initialized successfully!")
|
|
|
|
logger.Info("Basic app components loaded, continuing startup...")
|
|
}
|
|
|
|
func shouldStartBuiltInHotkeyListener() bool {
|
|
if runtime.GOOS != "linux" {
|
|
return true
|
|
}
|
|
return os.Getenv("WISFREE_USE_PORTAL_HOTKEY") == "1"
|
|
}
|
|
|
|
// Shutdown cleans up resources
|
|
func (a *App) Shutdown(ctx context.Context) {
|
|
if a.hotkeyListener != nil {
|
|
a.hotkeyListener.Stop()
|
|
}
|
|
if a.audioRecorder != nil {
|
|
a.audioRecorder.Cleanup()
|
|
}
|
|
if a.overlay != nil {
|
|
a.overlay.Close()
|
|
}
|
|
logger.Close()
|
|
}
|
|
|
|
// Greet returns a greeting for the given name (for frontend testing)
|
|
func (a *App) Greet(name string) string {
|
|
return fmt.Sprintf("Hello %s, It's show time!", name)
|
|
}
|
|
|
|
// CheckOnline checks if internet connection is available
|
|
func (a *App) CheckOnline() bool {
|
|
return whisper.CheckOnline()
|
|
}
|
|
|
|
// IsWhisperInstalled checks if offline whisper is installed
|
|
func (a *App) IsWhisperInstalled() bool {
|
|
mgr, err := whisper.NewManager()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return mgr.IsInstalled()
|
|
}
|
|
|
|
// GetWhisperInfo returns information about installed whisper
|
|
func (a *App) GetWhisperInfo() map[string]interface{} {
|
|
mgr, err := whisper.NewManager()
|
|
if err != nil {
|
|
return map[string]interface{}{"installed": false}
|
|
}
|
|
|
|
if !mgr.IsInstalled() {
|
|
return map[string]interface{}{"installed": false}
|
|
}
|
|
|
|
info, err := mgr.GetInstalledInfo()
|
|
if err != nil {
|
|
return map[string]interface{}{"installed": false}
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"installed": true,
|
|
"model": info.Model,
|
|
"version": info.Version,
|
|
}
|
|
}
|
|
|
|
// InstallWhisper starts the whisper installation process
|
|
func (a *App) InstallWhisper(model string) string {
|
|
mgr, err := whisper.NewManager()
|
|
if err != nil {
|
|
return fmt.Sprintf("Error: %v", err)
|
|
}
|
|
|
|
if err := mgr.Install(model); err != nil {
|
|
return fmt.Sprintf("Error: %v", err)
|
|
}
|
|
|
|
return "Installation started - check the terminal window"
|
|
}
|
|
|
|
// UninstallWhisper removes the whisper installation
|
|
func (a *App) UninstallWhisper() string {
|
|
mgr, err := whisper.NewManager()
|
|
if err != nil {
|
|
return fmt.Sprintf("Error: %v", err)
|
|
}
|
|
|
|
if err := mgr.Uninstall(); err != nil {
|
|
return fmt.Sprintf("Error: %v", err)
|
|
}
|
|
|
|
return "Whisper uninstalled successfully"
|
|
}
|
|
|
|
// GetAvailableWhisperModels returns list of available whisper models
|
|
func (a *App) GetAvailableWhisperModels() []map[string]string {
|
|
var models []map[string]string
|
|
for name, info := range whisper.Models {
|
|
models = append(models, map[string]string{
|
|
"name": name,
|
|
"size": info.Size,
|
|
})
|
|
}
|
|
return models
|
|
}
|