diff --git a/Api/GroqApiClient.cs b/Api/GroqApiClient.cs index cc95cf1..7122daf 100644 --- a/Api/GroqApiClient.cs +++ b/Api/GroqApiClient.cs @@ -20,7 +20,7 @@ public class GroqApiClient : ISpeechClient, ILlmClient _httpClient.BaseAddress = new Uri("https://api.groq.com/openai/v1/"); } - public async Task TranscribeAsync(string filePath, string language = "", string model = "whisper-large-v3-turbo") + public async Task TranscribeAsync(string filePath, string language = "", string model = Toak.Core.Constants.Defaults.WhisperModel) { using var content = new MultipartFormDataContent(); using var fileStream = File.OpenRead(filePath); @@ -29,7 +29,7 @@ public class GroqApiClient : ISpeechClient, ILlmClient streamContent.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); // or mpeg content.Add(streamContent, "file", Path.GetFileName(filePath)); - string modelToUse = string.IsNullOrWhiteSpace(model) ? "whisper-large-v3-turbo" : model; + string modelToUse = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.WhisperModel : model; // according to docs whisper-large-v3-turbo requires the language to be provided if it is to be translated later potentially or if we need the most accurate behavior // Actually, if we want language param, we can pass it to either model @@ -56,11 +56,11 @@ public class GroqApiClient : ISpeechClient, ILlmClient return result?.Text ?? string.Empty; } - public async Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = "openai/gpt-oss-20b") + public async Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel) { var requestBody = new LlamaRequest { - Model = string.IsNullOrWhiteSpace(model) ? "openai/gpt-oss-20b" : model, + Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model, Temperature = 0.0, Messages = new[] { @@ -87,11 +87,11 @@ public class GroqApiClient : ISpeechClient, ILlmClient return result?.Choices?.FirstOrDefault()?.Message?.Content ?? string.Empty; } - public async IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = "openai/gpt-oss-20b") + public async IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel) { var requestBody = new LlamaRequest { - Model = string.IsNullOrWhiteSpace(model) ? "openai/gpt-oss-20b" : model, + Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model, Temperature = 0.0, Stream = true, Messages = new[] diff --git a/Audio/AudioRecorder.cs b/Audio/AudioRecorder.cs index 7cb9910..5206d2e 100644 --- a/Audio/AudioRecorder.cs +++ b/Audio/AudioRecorder.cs @@ -9,7 +9,7 @@ namespace Toak.Audio; public class AudioRecorder : IAudioRecorder { - private readonly string WavPath = Path.Combine(Path.GetTempPath(), "toak_recording.wav"); + private readonly string WavPath = Constants.Paths.RecordingWavFile; private readonly IRecordingStateTracker _stateTracker; private readonly INotifications _notifications; @@ -33,7 +33,7 @@ public class AudioRecorder : IAudioRecorder var pInfo = new ProcessStartInfo { - FileName = "pw-record", + FileName = Constants.Commands.AudioRecord, Arguments = $"--rate=16000 --channels=1 --format=s16 \"{WavPath}\"", UseShellExecute = false, CreateNoWindow = true, @@ -63,7 +63,7 @@ public class AudioRecorder : IAudioRecorder // Gracefully stop pw-record using SIGINT to ensure WAV headers are finalizing cleanly Process.Start(new ProcessStartInfo { - FileName = "kill", + FileName = Constants.Commands.ProcessKill, Arguments = $"-INT {pid.Value}", CreateNoWindow = true, UseShellExecute = false diff --git a/Commands/LatencyTestCommand.cs b/Commands/LatencyTestCommand.cs index 290bf44..87cd09a 100644 --- a/Commands/LatencyTestCommand.cs +++ b/Commands/LatencyTestCommand.cs @@ -22,11 +22,11 @@ public static class LatencyTestCommand } AnsiConsole.MarkupLine("Generating 1-second silent audio file for testing..."); - var testWavPath = Path.Combine(Path.GetTempPath(), "toak_latency_test.wav"); + var testWavPath = Constants.Paths.LatencyTestWavFile; var pInfo = new ProcessStartInfo { - FileName = "ffmpeg", + FileName = Constants.Commands.AudioFfmpeg, Arguments = $"-f lavfi -i anullsrc=r=44100:cl=mono -t 1 -y {testWavPath}", UseShellExecute = false, CreateNoWindow = true, diff --git a/Configuration/ConfigManager.cs b/Configuration/ConfigManager.cs index c0da52e..3838e11 100644 --- a/Configuration/ConfigManager.cs +++ b/Configuration/ConfigManager.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Toak.Core; using Toak.Serialization; using Toak.Core.Interfaces; @@ -8,12 +9,11 @@ namespace Toak.Configuration; public class ConfigManager : IConfigProvider { - private readonly string ConfigDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "toak"); - private readonly string ConfigPath; + private readonly string ConfigDir = Constants.Paths.ConfigDir; + private readonly string ConfigPath = Constants.Paths.ConfigFile; public ConfigManager() { - ConfigPath = Path.Combine(ConfigDir, "config.json"); } public ToakConfig LoadConfig() diff --git a/Configuration/ToakConfig.cs b/Configuration/ToakConfig.cs index 6b1982d..f1e110d 100644 --- a/Configuration/ToakConfig.cs +++ b/Configuration/ToakConfig.cs @@ -8,8 +8,8 @@ public class ToakConfig public bool ModuleTechnicalSanitization { get; set; } = true; public string WhisperLanguage { get; set; } = string.Empty; - public string LlmModel { get; set; } = "openai/gpt-oss-20b"; - public string WhisperModel { get; set; } = "whisper-large-v3-turbo"; + public string LlmModel { get; set; } = Toak.Core.Constants.Defaults.LlmModel; + public string WhisperModel { get; set; } = Toak.Core.Constants.Defaults.WhisperModel; public string StartSoundPath { get; set; } = "Assets/Audio/beep.wav"; public string StopSoundPath { get; set; } = "Assets/Audio/beep.wav"; public List ActiveSkills { get; set; } = new List { "Terminal", "Translate" }; diff --git a/Core/Constants.cs b/Core/Constants.cs new file mode 100644 index 0000000..a15f90b --- /dev/null +++ b/Core/Constants.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; + +namespace Toak.Core; + +public static class Constants +{ + public const string AppName = "toak"; + + public static class Paths + { + public static readonly string AppDataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppName); + public static readonly string ConfigDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", AppName); + + public static readonly string ConfigFile = Path.Combine(ConfigDir, "config.json"); + public static readonly string HistoryFile = Path.Combine(AppDataDir, "history.jsonl"); + public static readonly string DaemonLockFile = Path.Combine(AppDataDir, "daemon.lock"); + + public static readonly string StateFile = Path.Combine(Path.GetTempPath(), "toak_state.pid"); + public static readonly string RecordingWavFile = Path.Combine(Path.GetTempPath(), "toak_recording.wav"); + public static readonly string LatencyTestWavFile = Path.Combine(Path.GetTempPath(), "toak_latency_test.wav"); + + public static string GetSocketPath() + { + var runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + return Path.Combine(string.IsNullOrEmpty(runtimeDir) ? Path.GetTempPath() : runtimeDir, "toak.sock"); + } + } + + public static class Commands + { + public const string AudioRecord = "pw-record"; + public const string AudioFfmpeg = "ffmpeg"; + public const string ProcessKill = "kill"; + public const string TypeX11 = "xdotool"; + public const string TypeWayland = "wtype"; + public const string Notify = "notify-send"; + public const string PlaySound = "paplay"; + public const string ClipboardX11 = "xclip"; + public const string ClipboardWayland = "wl-copy"; + } + + public static class Defaults + { + public const string LlmModel = "openai/gpt-oss-20b"; + public const string WhisperModel = "whisper-large-v3-turbo"; + } +} diff --git a/Core/DaemonService.cs b/Core/DaemonService.cs index 01824b9..041684a 100644 --- a/Core/DaemonService.cs +++ b/Core/DaemonService.cs @@ -15,19 +15,14 @@ public static class DaemonService { public static string GetSocketPath() { - var runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); - if (string.IsNullOrEmpty(runtimeDir)) - { - runtimeDir = Path.GetTempPath(); - } - return Path.Combine(runtimeDir, "toak.sock"); + return Constants.Paths.GetSocketPath(); } private static FileStream? _lockFile; public static async Task StartAsync(bool verbose) { - var lockPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "toak", "daemon.lock"); + var lockPath = Constants.Paths.DaemonLockFile; try { Directory.CreateDirectory(Path.GetDirectoryName(lockPath)!); diff --git a/Core/HistoryManager.cs b/Core/HistoryManager.cs index db1dafe..13367e4 100644 --- a/Core/HistoryManager.cs +++ b/Core/HistoryManager.cs @@ -10,12 +10,11 @@ namespace Toak.Core; public class HistoryManager : IHistoryManager { - private readonly string HistoryDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "toak"); - private readonly string HistoryFile; + private readonly string HistoryDir = Constants.Paths.AppDataDir; + private readonly string HistoryFile = Constants.Paths.HistoryFile; public HistoryManager() { - HistoryFile = Path.Combine(HistoryDir, "history.jsonl"); } public void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs) diff --git a/Core/Interfaces/Interfaces.cs b/Core/Interfaces/Interfaces.cs index ad0b818..97fc2f3 100644 --- a/Core/Interfaces/Interfaces.cs +++ b/Core/Interfaces/Interfaces.cs @@ -13,13 +13,13 @@ public interface IConfigProvider public interface ISpeechClient { - Task TranscribeAsync(string filePath, string language = "", string model = "whisper-large-v3-turbo"); + Task TranscribeAsync(string filePath, string language = "", string model = Toak.Core.Constants.Defaults.WhisperModel); } public interface ILlmClient { - Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = "openai/gpt-oss-20b"); - IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = "openai/gpt-oss-20b"); + Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel); + IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel); } public interface IAudioRecorder diff --git a/Core/StateTracker.cs b/Core/StateTracker.cs index b6e71c8..1f503b4 100644 --- a/Core/StateTracker.cs +++ b/Core/StateTracker.cs @@ -4,7 +4,7 @@ namespace Toak.Core; public class StateTracker : IRecordingStateTracker { - private readonly string StateFilePath = Path.Combine(Path.GetTempPath(), "toak_state.pid"); + private readonly string StateFilePath = Constants.Paths.StateFile; public bool IsRecording() { diff --git a/IO/ClipboardManager.cs b/IO/ClipboardManager.cs index 34c8fd5..6221b5f 100644 --- a/IO/ClipboardManager.cs +++ b/IO/ClipboardManager.cs @@ -25,7 +25,7 @@ public class ClipboardManager : IClipboardManager { pInfo = new ProcessStartInfo { - FileName = "wl-copy", + FileName = Toak.Core.Constants.Commands.ClipboardWayland, UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true @@ -35,7 +35,7 @@ public class ClipboardManager : IClipboardManager { pInfo = new ProcessStartInfo { - FileName = "xclip", + FileName = Toak.Core.Constants.Commands.ClipboardX11, Arguments = "-selection clipboard", UseShellExecute = false, CreateNoWindow = true, diff --git a/IO/Notifications.cs b/IO/Notifications.cs index 2a39b1c..98cd111 100644 --- a/IO/Notifications.cs +++ b/IO/Notifications.cs @@ -16,7 +16,7 @@ public class Notifications : INotifications { var pInfo = new ProcessStartInfo { - FileName = "notify-send", + FileName = Toak.Core.Constants.Commands.Notify, Arguments = $"-a \"Toak\" \"{summary}\" \"{body}\"", UseShellExecute = false, CreateNoWindow = true @@ -66,7 +66,7 @@ public class Notifications : INotifications var pInfo = new ProcessStartInfo { - FileName = "paplay", + FileName = Toak.Core.Constants.Commands.PlaySound, Arguments = $"\"{absolutePath}\"", UseShellExecute = false, CreateNoWindow = true diff --git a/IO/TextInjector.cs b/IO/TextInjector.cs index 3d4815c..31324ae 100644 --- a/IO/TextInjector.cs +++ b/IO/TextInjector.cs @@ -28,7 +28,7 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Injecting text using wtype..."); pInfo = new ProcessStartInfo { - FileName = "wtype", + FileName = Toak.Core.Constants.Commands.TypeWayland, Arguments = $"\"{text.Replace("\"", "\\\"")}\"", UseShellExecute = false, CreateNoWindow = true @@ -39,7 +39,7 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Injecting text using xdotool..."); pInfo = new ProcessStartInfo { - FileName = "xdotool", + FileName = Toak.Core.Constants.Commands.TypeX11, Arguments = $"type --clearmodifiers --delay 0 \"{text.Replace("\"", "\\\"")}\"", UseShellExecute = false, CreateNoWindow = true @@ -67,7 +67,7 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Setting up stream injection using wtype..."); pInfo = new ProcessStartInfo { - FileName = "wtype", + FileName = Toak.Core.Constants.Commands.TypeWayland, Arguments = "-", UseShellExecute = false, CreateNoWindow = true, @@ -79,7 +79,7 @@ public class TextInjector : ITextInjector Logger.LogDebug($"Setting up stream injection using xdotool..."); pInfo = new ProcessStartInfo { - FileName = "xdotool", + FileName = Toak.Core.Constants.Commands.TypeX11, Arguments = "type --clearmodifiers --delay 0 --file -", UseShellExecute = false, CreateNoWindow = true,