docs: update README with detailed desktop features and setup requirements
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const configFileName = "config.json"
|
||||
|
||||
type diskConfig struct {
|
||||
Version int `json:"version"`
|
||||
General struct {
|
||||
StartCaptions bool `json:"start_captions"`
|
||||
MainWindowAlwaysOnTop bool `json:"main_window_always_on_top"`
|
||||
} `json:"general"`
|
||||
Recognition struct {
|
||||
Mode Mode `json:"mode"`
|
||||
ChunkDuration string `json:"chunk_duration"`
|
||||
ModelsDir string `json:"models_directory"`
|
||||
Threads int `json:"threads"`
|
||||
PreviewThresholdDB float64 `json:"preview_threshold_dbfs"`
|
||||
VADThreshold float64 `json:"vad_threshold"`
|
||||
ModelAutoRestart bool `json:"model_auto_restart"`
|
||||
ModelTimeout string `json:"model_timeout"`
|
||||
ModelShutdownTimeout string `json:"model_shutdown_timeout"`
|
||||
} `json:"recognition"`
|
||||
Outputs struct {
|
||||
Overlay bool `json:"overlay"`
|
||||
History bool `json:"history"`
|
||||
StdoutCaptions bool `json:"stdout_captions"`
|
||||
StderrDiagnostics bool `json:"stderr_diagnostics"`
|
||||
} `json:"outputs"`
|
||||
Overlay struct {
|
||||
Backend OverlayBackend `json:"backend"`
|
||||
Opacity float64 `json:"opacity"`
|
||||
FontSize int `json:"font_size"`
|
||||
FontFamily string `json:"font_family"`
|
||||
BottomMargin int `json:"bottom_margin"`
|
||||
MaxWidth int `json:"max_width"`
|
||||
Monitor string `json:"monitor"`
|
||||
FinalTimeout string `json:"final_timeout"`
|
||||
ClickThrough bool `json:"click_through"`
|
||||
} `json:"overlay"`
|
||||
TranscriptLog struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Directory string `json:"directory"`
|
||||
} `json:"transcript_log"`
|
||||
}
|
||||
|
||||
func ConfigPath() (string, error) {
|
||||
base, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find user configuration directory: %w", err)
|
||||
}
|
||||
return filepath.Join(base, "captioneer", configFileName), nil
|
||||
}
|
||||
|
||||
func DefaultTranscriptDirectory() string {
|
||||
if stateHome := os.Getenv("XDG_STATE_HOME"); filepath.IsAbs(stateHome) {
|
||||
return filepath.Join(stateHome, "captioneer", "transcripts")
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return filepath.Join("captioneer", "transcripts")
|
||||
}
|
||||
return filepath.Join(home, ".local", "state", "captioneer", "transcripts")
|
||||
}
|
||||
|
||||
func Load(path string) (AppConfig, error) {
|
||||
defaults := DefaultAppConfig()
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return defaults, nil
|
||||
}
|
||||
if err != nil {
|
||||
return defaults, fmt.Errorf("read configuration: %w", err)
|
||||
}
|
||||
|
||||
disk := diskFromConfig(defaults)
|
||||
if err := json.Unmarshal(data, &disk); err != nil {
|
||||
return defaults, fmt.Errorf("parse configuration: %w", err)
|
||||
}
|
||||
loaded, err := disk.toConfig()
|
||||
if err != nil {
|
||||
return defaults, err
|
||||
}
|
||||
if err := loaded.Validate(); err != nil {
|
||||
return defaults, fmt.Errorf("validate configuration: %w", err)
|
||||
}
|
||||
return loaded, nil
|
||||
}
|
||||
|
||||
func Save(path string, config AppConfig) error {
|
||||
config.Version = CurrentConfigVersion
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validate configuration: %w", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(diskFromConfig(config), "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode configuration: %w", err)
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
directory := filepath.Dir(path)
|
||||
if err := os.MkdirAll(directory, 0o700); err != nil {
|
||||
return fmt.Errorf("create configuration directory: %w", err)
|
||||
}
|
||||
temporary, err := os.CreateTemp(directory, ".config-*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary configuration: %w", err)
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if err := temporary.Chmod(0o600); err != nil {
|
||||
temporary.Close()
|
||||
return fmt.Errorf("secure temporary configuration: %w", err)
|
||||
}
|
||||
if _, err := temporary.Write(data); err != nil {
|
||||
temporary.Close()
|
||||
return fmt.Errorf("write temporary configuration: %w", err)
|
||||
}
|
||||
if err := temporary.Sync(); err != nil {
|
||||
temporary.Close()
|
||||
return fmt.Errorf("sync temporary configuration: %w", err)
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return fmt.Errorf("close temporary configuration: %w", err)
|
||||
}
|
||||
if err := os.Rename(temporaryPath, path); err != nil {
|
||||
return fmt.Errorf("replace configuration: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func diskFromConfig(config AppConfig) diskConfig {
|
||||
var disk diskConfig
|
||||
disk.Version = config.Version
|
||||
disk.General.StartCaptions = config.GUI.StartCaptions
|
||||
disk.General.MainWindowAlwaysOnTop = config.GUI.MainWindowAlwaysOnTop
|
||||
disk.Recognition.Mode = config.Settings.Mode
|
||||
disk.Recognition.ChunkDuration = config.Settings.ChunkDuration.String()
|
||||
disk.Recognition.ModelsDir = config.Settings.ModelsDir
|
||||
disk.Recognition.Threads = config.Settings.Threads
|
||||
disk.Recognition.PreviewThresholdDB = config.Settings.PreviewThresholdDB
|
||||
disk.Recognition.VADThreshold = config.Settings.VADThreshold
|
||||
disk.Recognition.ModelAutoRestart = config.Settings.ModelAutoRestart
|
||||
disk.Recognition.ModelTimeout = config.Settings.ModelTimeout.String()
|
||||
disk.Recognition.ModelShutdownTimeout = config.Settings.ModelShutdownTimeout.String()
|
||||
disk.Outputs.Overlay = config.Outputs.Overlay
|
||||
disk.Outputs.History = config.Outputs.History
|
||||
disk.Outputs.StdoutCaptions = config.Outputs.StdoutCaptions
|
||||
disk.Outputs.StderrDiagnostics = config.Outputs.StderrDiagnostics
|
||||
disk.Overlay.Backend = config.Overlay.Backend
|
||||
disk.Overlay.Opacity = config.Overlay.Opacity
|
||||
disk.Overlay.FontSize = config.Overlay.FontSize
|
||||
disk.Overlay.FontFamily = config.Overlay.FontFamily
|
||||
disk.Overlay.BottomMargin = config.Overlay.BottomMargin
|
||||
disk.Overlay.MaxWidth = config.Overlay.MaxWidth
|
||||
disk.Overlay.Monitor = config.Overlay.Monitor
|
||||
disk.Overlay.FinalTimeout = config.Overlay.FinalTimeout.String()
|
||||
disk.Overlay.ClickThrough = config.Overlay.ClickThrough
|
||||
disk.TranscriptLog.Enabled = config.TranscriptLog.Enabled
|
||||
disk.TranscriptLog.Directory = config.TranscriptLog.Directory
|
||||
return disk
|
||||
}
|
||||
|
||||
func (disk diskConfig) toConfig() (AppConfig, error) {
|
||||
chunkDuration, err := parseStoredDuration("chunk duration", disk.Recognition.ChunkDuration)
|
||||
if err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
modelTimeout, err := parseStoredDuration("model timeout", disk.Recognition.ModelTimeout)
|
||||
if err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
shutdownTimeout, err := parseStoredDuration("model shutdown timeout", disk.Recognition.ModelShutdownTimeout)
|
||||
if err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
finalTimeout, err := parseStoredDuration("overlay final timeout", disk.Overlay.FinalTimeout)
|
||||
if err != nil {
|
||||
return AppConfig{}, err
|
||||
}
|
||||
return AppConfig{
|
||||
Version: disk.Version,
|
||||
GUI: GUISettings{
|
||||
StartCaptions: disk.General.StartCaptions,
|
||||
MainWindowAlwaysOnTop: disk.General.MainWindowAlwaysOnTop,
|
||||
},
|
||||
Settings: Settings{
|
||||
Mode: disk.Recognition.Mode,
|
||||
ChunkDuration: chunkDuration,
|
||||
ModelsDir: disk.Recognition.ModelsDir,
|
||||
Threads: disk.Recognition.Threads,
|
||||
PreviewThresholdDB: disk.Recognition.PreviewThresholdDB,
|
||||
VADThreshold: disk.Recognition.VADThreshold,
|
||||
ModelAutoRestart: disk.Recognition.ModelAutoRestart,
|
||||
ModelTimeout: modelTimeout,
|
||||
ModelShutdownTimeout: shutdownTimeout,
|
||||
},
|
||||
Outputs: OutputSettings{
|
||||
Overlay: disk.Outputs.Overlay,
|
||||
History: disk.Outputs.History,
|
||||
StdoutCaptions: disk.Outputs.StdoutCaptions,
|
||||
StderrDiagnostics: disk.Outputs.StderrDiagnostics,
|
||||
},
|
||||
Overlay: OverlaySettings{
|
||||
Backend: disk.Overlay.Backend,
|
||||
Opacity: disk.Overlay.Opacity,
|
||||
FontSize: disk.Overlay.FontSize,
|
||||
FontFamily: disk.Overlay.FontFamily,
|
||||
BottomMargin: disk.Overlay.BottomMargin,
|
||||
MaxWidth: disk.Overlay.MaxWidth,
|
||||
Monitor: disk.Overlay.Monitor,
|
||||
FinalTimeout: finalTimeout,
|
||||
ClickThrough: disk.Overlay.ClickThrough,
|
||||
},
|
||||
TranscriptLog: TranscriptLogSettings{
|
||||
Enabled: disk.TranscriptLog.Enabled,
|
||||
Directory: disk.TranscriptLog.Directory,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseStoredDuration(name, value string) (time.Duration, error) {
|
||||
duration, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse %s %q: %w", name, value, err)
|
||||
}
|
||||
return duration, nil
|
||||
}
|
||||
Reference in New Issue
Block a user