Ariver
2026-07-06 7ed43baaa6be88a55952a3411575922c8d883a08
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package config
 
import (
    "encoding/json"
    "os"
    "runtime"
    "voicesnap/internal/logger"
    "voicesnap/internal/model"
    "voicesnap/internal/paths"
)
 
const (
    LegacyDefaultHotkeyVK    = 0xA5
    RightModifierDefaultVK   = 0x5C
    HotkeyModeTap            = "tap"
    HotkeyModeHold           = "hold"
    LanguageModeAuto         = "auto"
    LanguageModeManual       = "manual"
    ModelSelectionModeAuto   = "auto"
    ModelSelectionModeManual = "manual"
)
 
// Config holds the application configuration, compatible with the WPF version's config.json.
type Config struct {
    HotkeyVK                 int    `json:"HotkeyVK"`
    HotkeyMode               string `json:"HotkeyMode"`
    AutoHide                 bool   `json:"AutoHide"`
    SoundFeedback            bool   `json:"SoundFeedback"`
    HideDockIcon             bool   `json:"HideDockIcon"`
    CopyToClipboard          bool   `json:"CopyToClipboard"`
    DeviceName               string `json:"DeviceName,omitempty"`
    IndicatorX               int    `json:"IndicatorX,omitempty"`
    IndicatorY               int    `json:"IndicatorY,omitempty"`
    SelectedModelID          string `json:"SelectedModelID,omitempty"`
    ModelSelectionMode       string `json:"ModelSelectionMode,omitempty"`
    LanguageMode             string `json:"LanguageMode,omitempty"`
    LanguageID               string `json:"LanguageID,omitempty"`
    ModelDownloadUrl         string `json:"ModelDownloadUrl"`
    FallbackModelDownloadUrl string `json:"FallbackModelDownloadUrl"`
}
 
// Default returns a default configuration.
func Default() *Config {
    return defaultWithHotkey(defaultHotkeyVKForNewConfig(runtime.GOOS))
}
 
func legacyDefaultForExistingConfig() *Config {
    return defaultWithHotkey(LegacyDefaultHotkeyVK)
}
 
// ExistingConfigFallback returns a legacy-safe fallback for cases where a
// config file exists but cannot be read or parsed.
func ExistingConfigFallback() *Config {
    return legacyDefaultForExistingConfig()
}
 
func defaultWithHotkey(hotkeyVK int) *Config {
    return &Config{
        HotkeyVK:                 hotkeyVK,
        HotkeyMode:               HotkeyModeHold,
        AutoHide:                 true,
        SoundFeedback:            true,
        HideDockIcon:             true,
        CopyToClipboard:          true,
        SelectedModelID:          model.DefaultModelID,
        ModelSelectionMode:       ModelSelectionModeAuto,
        LanguageMode:             LanguageModeAuto,
        LanguageID:               model.DefaultLanguageID,
        ModelDownloadUrl:         "http://www.maikami.com/voicesnap/sensevoice.zip",
        FallbackModelDownloadUrl: "https://modelscope.cn/models/sherpa-onnx/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2",
    }
}
 
func defaultHotkeyVKForNewConfig(goos string) int {
    switch goos {
    case "darwin", "windows":
        return RightModifierDefaultVK
    default:
        return LegacyDefaultHotkeyVK
    }
}
 
func configPath() string {
    return paths.File("config.json")
}
 
// Load reads the config from disk. Returns default config on error.
func Load() (*Config, error) {
    path := configPath()
    data, err := os.ReadFile(path)
    if err != nil {
        if os.IsNotExist(err) {
            cfg := Default()
            Save(cfg)
            return cfg, nil
        }
        return nil, err
    }
 
    cfg := legacyDefaultForExistingConfig()
    if err := json.Unmarshal(data, cfg); err != nil {
        return nil, err
    }
    cfg.HotkeyMode = NormalizeHotkeyMode(cfg.HotkeyMode)
    cfg.LanguageMode = NormalizeLanguageMode(cfg.LanguageMode)
    cfg.ModelSelectionMode = NormalizeModelSelectionMode(cfg.ModelSelectionMode)
    languageID := cfg.LanguageID
    cfg.LanguageID = model.NormalizeLanguageID(cfg.LanguageID)
    if languageID != "" && languageID != cfg.LanguageID {
        logger.Info("Unknown LanguageID %q, falling back to %q", languageID, cfg.LanguageID)
    }
    selectedModelID := cfg.SelectedModelID
    cfg.SelectedModelID = model.NormalizeModelID(cfg.SelectedModelID)
    if selectedModelID != "" && selectedModelID != cfg.SelectedModelID {
        logger.Info("Unknown SelectedModelID %q, falling back to %q", selectedModelID, cfg.SelectedModelID)
    }
    return cfg, nil
}
 
func NormalizeLanguageMode(mode string) string {
    switch mode {
    case LanguageModeAuto, LanguageModeManual:
        return mode
    default:
        return LanguageModeAuto
    }
}
 
func NormalizeModelSelectionMode(mode string) string {
    switch mode {
    case ModelSelectionModeAuto, ModelSelectionModeManual:
        return mode
    default:
        return ModelSelectionModeAuto
    }
}
 
func NormalizeHotkeyMode(mode string) string {
    switch mode {
    case HotkeyModeTap, HotkeyModeHold:
        return mode
    default:
        return HotkeyModeHold
    }
}
 
// Save writes the config to disk.
func Save(cfg *Config) {
    if err := paths.Ensure(); err != nil {
        logger.Error("Failed to create app data dir: %v", err)
        return
    }
 
    data, err := json.MarshalIndent(cfg, "", "  ")
    if err != nil {
        logger.Error("Failed to marshal config: %v", err)
        return
    }
    if err := os.WriteFile(configPath(), data, 0644); err != nil {
        logger.Error("Failed to save config: %v", err)
    }
}