feat: add backend configuration and windows utilities, and initialize frontend dependencies

This commit is contained in:
jahruz67
2026-05-15 10:39:30 -07:00
parent e645102d3a
commit b0f4ea3e6f
43 changed files with 931 additions and 541 deletions
+4 -2
View File
@@ -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 {
+25 -4
View File
@@ -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.
+8 -3
View File
@@ -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,
+8 -4
View File
@@ -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)
+8 -3
View File
@@ -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
View File
@@ -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 {
+3 -1
View File
@@ -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)
}