mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
DUMP
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/audio/media"
|
||||
"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/services/transcriber"
|
||||
"wis-free-v3/internal/services/whisper"
|
||||
"wis-free-v3/internal/system/startup"
|
||||
"wis-free-v3/internal/ui/overlay"
|
||||
"wis-free-v3/internal/ui/tray"
|
||||
|
||||
"github.com/go-vgo/robotgo"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
audioRecorder *recorder.AudioRecorder
|
||||
hotkeyListener *hotkey.Listener
|
||||
transcriber *transcriber.Client
|
||||
config *config.Config
|
||||
overlay *overlay.Overlay
|
||||
recordingPath string
|
||||
isQuitting bool
|
||||
wasMediaPlaying bool
|
||||
whisperManager *whisper.Manager
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ShowSettings shows the settings window
|
||||
func (a *App) ShowSettings() {
|
||||
if a.ctx != nil {
|
||||
runtime.WindowShow(a.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called when the app starts
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
// Initialize components
|
||||
a.startupHeadless()
|
||||
|
||||
// Start system tray in a goroutine
|
||||
go tray.Start(a)
|
||||
}
|
||||
|
||||
// beforeClose is called when the window is about to close
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
if a.isQuitting {
|
||||
return false
|
||||
}
|
||||
runtime.WindowHide(ctx)
|
||||
return true
|
||||
}
|
||||
|
||||
// Quit handles application exit from tray
|
||||
func (a *App) Quit() {
|
||||
a.isQuitting = true
|
||||
logger.Info("Application quitting...")
|
||||
logger.Close()
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
// StartRecording starts the audio recording
|
||||
func (a *App) StartRecording() {
|
||||
logger.Info("StartRecording triggered")
|
||||
|
||||
// Pause media if playing
|
||||
a.wasMediaPlaying = media.PauseMedia()
|
||||
if a.wasMediaPlaying {
|
||||
logger.Info("Media paused for recording")
|
||||
}
|
||||
|
||||
tray.UpdateStatus("Recording...")
|
||||
if a.overlay != nil {
|
||||
a.overlay.Show("Recording...")
|
||||
}
|
||||
|
||||
if a.audioRecorder == nil {
|
||||
logger.Error("Recorder not initialized")
|
||||
tray.UpdateStatus("Ready")
|
||||
if a.overlay != nil {
|
||||
a.overlay.Hide()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Save to a temporary file
|
||||
tempDir := os.TempDir()
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
a.recordingPath = filepath.Join(tempDir, fmt.Sprintf("wis_recording_%s.wav", timestamp))
|
||||
|
||||
err := a.audioRecorder.Start(a.recordingPath)
|
||||
if err != nil {
|
||||
logger.Error("Failed to start recording with primary device: %v", err)
|
||||
|
||||
// IDIOT-PROOFING: Fallback to default microphone if the selected one fails
|
||||
if a.config.MicrophoneDevice != nil {
|
||||
logger.Info("Attempting fallback to default microphone...")
|
||||
a.audioRecorder.SetDevice("") // Reset to default
|
||||
err = a.audioRecorder.Start(a.recordingPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Recording completely failed: %v", err)
|
||||
tray.UpdateStatus("Ready")
|
||||
if a.overlay != nil {
|
||||
a.overlay.Hide()
|
||||
}
|
||||
// Resume media if we paused it
|
||||
media.ResumeMedia(a.wasMediaPlaying)
|
||||
return
|
||||
}
|
||||
logger.Info("Fallback successful - using default microphone")
|
||||
}
|
||||
}
|
||||
|
||||
// StopRecording stops the audio recording and triggers transcription
|
||||
func (a *App) StopRecording() {
|
||||
logger.Info("StopRecording triggered")
|
||||
|
||||
// Resume media if it was playing before
|
||||
media.ResumeMedia(a.wasMediaPlaying)
|
||||
if a.wasMediaPlaying {
|
||||
logger.Info("Media resumed after recording")
|
||||
}
|
||||
|
||||
if a.audioRecorder == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.audioRecorder.Stop()
|
||||
if err != nil {
|
||||
logger.Error("Failed to stop recording: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Transcribe in a goroutine to avoid blocking
|
||||
go a.processRecording()
|
||||
}
|
||||
|
||||
// processRecording handles transcription and pasting
|
||||
func (a *App) processRecording() {
|
||||
if a.recordingPath == "" {
|
||||
logger.Error("No recording path set")
|
||||
tray.UpdateStatus("Ready")
|
||||
if a.overlay != nil {
|
||||
a.overlay.Hide()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Transcribing audio...")
|
||||
tray.UpdateStatus("Transcribing...")
|
||||
if a.overlay != nil {
|
||||
a.overlay.Show("Transcribing...")
|
||||
}
|
||||
|
||||
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(a.recordingPath)
|
||||
}
|
||||
} else {
|
||||
// Use cloud API
|
||||
text, err = a.transcriber.TranscribeAudio(a.recordingPath, a.config.Language)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Transcription failed: %v", err)
|
||||
tray.UpdateStatus("Ready")
|
||||
if a.overlay != nil {
|
||||
a.overlay.Hide()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Transcribed: %s", text)
|
||||
|
||||
// Refine text (optional)
|
||||
refinedText, err := a.transcriber.RefineText(text)
|
||||
if err != nil {
|
||||
logger.Error("Refinement failed: %v", err)
|
||||
// Fallback to original text
|
||||
refinedText = text
|
||||
} else {
|
||||
logger.Info("Refined: %s", refinedText)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
historyItem := config.HistoryItem{
|
||||
Text: refinedText,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
// Prepend to history
|
||||
a.config.History = append([]config.HistoryItem{historyItem}, a.config.History...)
|
||||
// Keep only last 50 items
|
||||
if len(a.config.History) > 50 {
|
||||
a.config.History = a.config.History[:50]
|
||||
}
|
||||
config.Save(a.config, "")
|
||||
|
||||
// Copy to clipboard
|
||||
clipboard.Write(clipboard.FmtText, []byte(refinedText))
|
||||
|
||||
// Paste
|
||||
a.pasteText()
|
||||
|
||||
// Clean up the recording file
|
||||
os.Remove(a.recordingPath)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// pasteText simulates Ctrl+V to paste from clipboard
|
||||
func (a *App) pasteText() {
|
||||
// Give a small delay for clipboard to update
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Simulate Ctrl+V
|
||||
robotgo.KeyToggle("control", "down")
|
||||
robotgo.KeyTap("v")
|
||||
robotgo.KeyToggle("control", "up")
|
||||
}
|
||||
|
||||
// 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"] = startup.IsInStartup()
|
||||
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
|
||||
}
|
||||
if val, ok := settings["shortcut"].(string); ok {
|
||||
// Validate shortcut before applying
|
||||
trigger, _ := hotkey.ParseShortcut(val)
|
||||
if len(trigger) == 0 {
|
||||
logger.Error("Invalid shortcut: %s (rejected)", val)
|
||||
return "Invalid shortcut - must have at least one modifier and a regular key"
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
config.Save(a.config, "")
|
||||
|
||||
// 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 = startup.AddToStartup()
|
||||
} else {
|
||||
err = startup.RemoveFromStartup()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Startup toggle error: %v", err)
|
||||
return fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Initialize clipboard
|
||||
err = clipboard.Init()
|
||||
if err != nil {
|
||||
logger.Error("Failed to initialize clipboard: %v", 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 in background for instant startup
|
||||
go func() {
|
||||
// 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 = overlay.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)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Hotkey Listener
|
||||
a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.StartRecording, a.StopRecording)
|
||||
a.hotkeyListener.Start()
|
||||
|
||||
logger.Info("Background components initialized successfully!")
|
||||
}()
|
||||
|
||||
logger.Info("Basic app components loaded, continuing startup...")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user