mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
feat: add backend configuration and windows utilities, and initialize frontend dependencies
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"math"
|
||||
"unsafe"
|
||||
@@ -215,7 +216,8 @@ func (r *AudioRecorder) Stop() error {
|
||||
if r.outputFile != nil {
|
||||
// Rewrite header with correct data size
|
||||
r.outputFile.Seek(0, 0)
|
||||
if err := r.writeWAVHeader(r.dataSize); err != nil {
|
||||
finalSize := atomic.LoadUint32(&r.dataSize)
|
||||
if err := r.writeWAVHeader(finalSize); err != nil {
|
||||
logger.Error("Failed to update WAV header: %v", err)
|
||||
}
|
||||
r.outputFile.Close()
|
||||
@@ -256,7 +258,7 @@ func (r *AudioRecorder) Cleanup() {
|
||||
func (r *AudioRecorder) onAudioData(_, inputSamples []byte, _ uint32) {
|
||||
if r.outputFile != nil && len(inputSamples) > 0 {
|
||||
n, _ := r.outputFile.Write(inputSamples)
|
||||
r.dataSize += uint32(n)
|
||||
atomic.AddUint32(&r.dataSize, uint32(n))
|
||||
|
||||
// Calculate volume if callback is set
|
||||
if r.OnVolume != nil {
|
||||
|
||||
@@ -29,6 +29,7 @@ type HistoryItem struct {
|
||||
|
||||
// Config represents the complete application configuration.
|
||||
type Config struct {
|
||||
Version int `json:"version"`
|
||||
APIKey string `json:"api_key"`
|
||||
Shortcut string `json:"shortcut"`
|
||||
WhisperModel string `json:"whisper_model"`
|
||||
@@ -39,6 +40,9 @@ type Config struct {
|
||||
History []HistoryItem `json:"history"`
|
||||
}
|
||||
|
||||
const CurrentConfigVersion = 1
|
||||
const MaxHistoryItems = 100
|
||||
|
||||
// DefaultConfig returns a new configuration with sensible default values.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
@@ -70,12 +74,21 @@ func Load(configPath string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply defaults for any missing fields
|
||||
// Apply defaults and migrations
|
||||
cfg.migrate()
|
||||
cfg.applyDefaults()
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// migrate handles version-based configuration upgrades
|
||||
func (c *Config) migrate() {
|
||||
if c.Version < CurrentConfigVersion {
|
||||
// Migration logic for future versions goes here
|
||||
c.Version = CurrentConfigVersion
|
||||
}
|
||||
}
|
||||
|
||||
// Save writes the configuration to the specified file path.
|
||||
// If configPath is empty, it uses the default configuration path.
|
||||
func Save(c *Config, configPath string) error {
|
||||
@@ -134,12 +147,20 @@ func (c *Config) applyDefaults() {
|
||||
}
|
||||
}
|
||||
|
||||
// AddHistoryItem adds a new transcription to the history.
|
||||
// AddHistoryItem adds a new transcription to the history, enforcing a maximum limit.
|
||||
func (c *Config) AddHistoryItem(text, timestamp string) {
|
||||
c.History = append(c.History, HistoryItem{
|
||||
newItem := HistoryItem{
|
||||
Text: text,
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
// Prepend to history so the newest items are at the top
|
||||
c.History = append([]HistoryItem{newItem}, c.History...)
|
||||
|
||||
// Bounded history: keep only the most recent items
|
||||
if len(c.History) > MaxHistoryItems {
|
||||
c.History = c.History[:MaxHistoryItems]
|
||||
}
|
||||
}
|
||||
|
||||
// ClearHistory removes all history items.
|
||||
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
const (
|
||||
DefaultWhisperModel = "whisper-large-v3-turbo"
|
||||
DefaultAIModel = "llama-3.3-70b-versatile"
|
||||
HTTPTimeout = 60 * time.Second
|
||||
HTTPTimeout = 300 * time.Second
|
||||
RefinementTemp = 0.3 // Temperature for text refinement (lower = more deterministic)
|
||||
)
|
||||
|
||||
@@ -118,15 +118,20 @@ func (c *Client) TranscribeAudio(audioFilePath, language string) (string, error)
|
||||
|
||||
// RefineText uses an LLM to clean up and correct transcribed text.
|
||||
// If the AI model is set to "None" or the API key is missing, returns the original text.
|
||||
func (c *Client) RefineText(text string) (string, error) {
|
||||
func (c *Client) RefineText(text string, activeContext string) (string, error) {
|
||||
if c.apiKey == "" || c.aiModel == "None" {
|
||||
return text, nil
|
||||
}
|
||||
|
||||
systemPrompt := c.aiPrompt
|
||||
if activeContext != "" {
|
||||
systemPrompt += fmt.Sprintf("\n\nContext: The user is currently typing in a window titled '%s'. Please adapt the formatting appropriately (e.g. casual for chat apps, formal for email, code comments for IDEs). Do not mention the window title, just format appropriately.", activeContext)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"model": c.aiModel,
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": c.aiPrompt},
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{"role": "user", "content": text},
|
||||
},
|
||||
"temperature": RefinementTemp,
|
||||
|
||||
@@ -261,7 +261,7 @@ func (m *Manager) Uninstall() error {
|
||||
}
|
||||
|
||||
// Transcribe runs local whisper transcription
|
||||
func (m *Manager) Transcribe(audioPath string) (string, error) {
|
||||
func (m *Manager) Transcribe(audioPath string, language string) (string, error) {
|
||||
if !m.IsInstalled() {
|
||||
return "", fmt.Errorf("whisper is not installed")
|
||||
}
|
||||
@@ -280,11 +280,15 @@ func (m *Manager) Transcribe(audioPath string) (string, error) {
|
||||
modelPath = filepath.Join(m.installDir, modelFilename)
|
||||
}
|
||||
|
||||
logger.Info("Running whisper: %s -m %s -f %s", binaryPath, modelPath, audioPath)
|
||||
if language == "" {
|
||||
language = "auto"
|
||||
}
|
||||
|
||||
// Run whisper with simple arguments: ./main -m model.bin -f audio.wav
|
||||
logger.Info("Running whisper: %s -m %s -l %s -f %s", binaryPath, modelPath, language, audioPath)
|
||||
|
||||
// Run whisper with simple arguments: ./main -m model.bin -l language -f audio.wav
|
||||
// Vulkan build uses GPU by default, no need for -ngl
|
||||
cmd := exec.Command(binaryPath, "-m", modelPath, "-f", audioPath)
|
||||
cmd := exec.Command(binaryPath, "-m", modelPath, "-l", language, "-f", audioPath)
|
||||
|
||||
// Set working directory to the binary's location so it can find DLLs
|
||||
cmd.Dir = filepath.Dir(binaryPath)
|
||||
|
||||
@@ -67,8 +67,10 @@ func onReady(app App) {
|
||||
statusMenuItem = systray.AddMenuItem("Status: Ready", "Current application status")
|
||||
statusMenuItem.Disable()
|
||||
|
||||
triggerCountItem = systray.AddMenuItem("Shortcut detected: 0 times", "Troubleshooting counter")
|
||||
triggerCountItem.Disable()
|
||||
if runtime.GOOS != "windows" {
|
||||
triggerCountItem = systray.AddMenuItem("Shortcut detected: 0 times", "Troubleshooting counter")
|
||||
triggerCountItem.Disable()
|
||||
}
|
||||
|
||||
systray.AddSeparator()
|
||||
|
||||
@@ -187,9 +189,12 @@ func icoToPNG(data []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("no PNG image found in ico")
|
||||
}
|
||||
|
||||
var lastStatus string
|
||||
|
||||
// UpdateStatus updates the status text displayed in the tray menu.
|
||||
func UpdateStatus(status string) {
|
||||
if statusMenuItem != nil {
|
||||
if statusMenuItem != nil && status != lastStatus {
|
||||
lastStatus = status
|
||||
statusMenuItem.SetTitle("Status: " + status)
|
||||
systray.SetTooltip(trayLabel + " - " + status)
|
||||
|
||||
|
||||
+15
-12
@@ -5,7 +5,6 @@ import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"wis-free-v3/internal/logger"
|
||||
)
|
||||
@@ -41,17 +40,21 @@ func sendKey(key byte) {
|
||||
|
||||
// IsPlaying checks if media is currently playing using PowerShell SMTC query
|
||||
func IsPlaying() bool {
|
||||
// Write script to temp file only if it doesn't exist
|
||||
tempDir := os.TempDir()
|
||||
scriptPath := filepath.Join(tempDir, "wis_check_media_v2.ps1")
|
||||
|
||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||
err = os.WriteFile(scriptPath, checkMediaScript, 0644)
|
||||
if err != nil {
|
||||
logger.Error("Failed to write media check script: %v", err)
|
||||
return false
|
||||
}
|
||||
// Create a secure temp file for the script to prevent symlink attacks
|
||||
f, err := os.CreateTemp("", "wis_check_media_*.ps1")
|
||||
if err != nil {
|
||||
logger.Error("Failed to create secure media check script: %v", err)
|
||||
return false
|
||||
}
|
||||
scriptPath := f.Name()
|
||||
defer os.Remove(scriptPath) // Clean up immediately after execution
|
||||
|
||||
if _, err := f.Write(checkMediaScript); err != nil {
|
||||
f.Close()
|
||||
logger.Error("Failed to write media check script: %v", err)
|
||||
return false
|
||||
}
|
||||
f.Close()
|
||||
|
||||
cmd := exec.Command("powershell.exe",
|
||||
"-NoProfile",
|
||||
@@ -67,7 +70,7 @@ func IsPlaying() bool {
|
||||
CreationFlags: 0x08000000 | 0x00000200, // CREATE_NO_WINDOW | DETACHED_PROCESS
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
err = cmd.Run()
|
||||
|
||||
// Exit code 0 = not playing, Exit code 1 = playing
|
||||
if err == nil {
|
||||
|
||||
@@ -33,7 +33,9 @@ func AddToStartup() error {
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
if err := key.SetStringValue(appName, exePath); err != nil {
|
||||
// Quote the path to handle spaces correctly and prevent execution hijacking
|
||||
quotedPath := fmt.Sprintf("\"%s\"", exePath)
|
||||
if err := key.SetStringValue(appName, quotedPath); err != nil {
|
||||
return fmt.Errorf("failed to set registry value: %w", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user