1
0

refactor: modernize code, improve performance, and clean up various components.

This commit is contained in:
2026-03-01 21:05:35 +01:00
parent 15f9647f8a
commit a6c7df0a71
37 changed files with 240 additions and 627 deletions

View File

@@ -15,7 +15,7 @@ public class OpenAiRequest
[JsonPropertyName("model")] [JsonPropertyName("model")]
public string Model { get; set; } = "llama-3.1-8b-instant"; public string Model { get; set; } = "llama-3.1-8b-instant";
[JsonPropertyName("messages")] [JsonPropertyName("messages")]
public OpenAiRequestMessage[] Messages { get; set; } = Array.Empty<OpenAiRequestMessage>(); public OpenAiRequestMessage[] Messages { get; set; } = [];
[JsonPropertyName("temperature")] [JsonPropertyName("temperature")]
public double Temperature { get; set; } = 0.0; public double Temperature { get; set; } = 0.0;
[JsonPropertyName("stream")] [JsonPropertyName("stream")]
@@ -27,7 +27,7 @@ public class OpenAiRequest
public class OpenAiResponse public class OpenAiResponse
{ {
[JsonPropertyName("choices")] [JsonPropertyName("choices")]
public OpenAiChoice[] Choices { get; set; } = Array.Empty<OpenAiChoice>(); public OpenAiChoice[] Choices { get; set; } = [];
} }
public class OpenAiChoice public class OpenAiChoice
@@ -39,7 +39,7 @@ public class OpenAiChoice
public class OpenAiStreamResponse public class OpenAiStreamResponse
{ {
[JsonPropertyName("choices")] [JsonPropertyName("choices")]
public OpenAiStreamChoice[] Choices { get; set; } = Array.Empty<OpenAiStreamChoice>(); public OpenAiStreamChoice[] Choices { get; set; } = [];
} }
public class OpenAiStreamChoice public class OpenAiStreamChoice

View File

@@ -1,7 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Toak.Api.Models; using Toak.Api.Models;
using Toak.Serialization; using Toak.Serialization;
using Toak.Core; using Toak.Core;
@@ -22,17 +20,17 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient
_reasoningEffort = reasoningEffort == "none" ? null : reasoningEffort; _reasoningEffort = reasoningEffort == "none" ? null : reasoningEffort;
} }
public async Task<string> TranscribeAsync(string filePath, string language = "", string model = Toak.Core.Constants.Defaults.WhisperModel) public async Task<string> TranscribeAsync(string filePath, string language = "", string model = Constants.Defaults.WhisperModel)
{ {
// ... (TranscribeAsync content remains same except maybe some internal comments or contexts) // ... (TranscribeAsync content remains same except maybe some internal comments or contexts)
using var content = new MultipartFormDataContent(); using var content = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath); await using var fileStream = File.OpenRead(filePath);
using var streamContent = new StreamContent(fileStream); using var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); // or mpeg streamContent.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); // or mpeg
content.Add(streamContent, "file", Path.GetFileName(filePath)); content.Add(streamContent, "file", Path.GetFileName(filePath));
string modelToUse = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.WhisperModel : model; var modelToUse = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.WhisperModel : model;
content.Add(new StringContent(modelToUse), "model"); content.Add(new StringContent(modelToUse), "model");
@@ -57,18 +55,18 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient
return result?.Text ?? string.Empty; return result?.Text ?? string.Empty;
} }
public async Task<string> RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel) public async Task<string> RefineTextAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel)
{ {
var requestBody = new OpenAiRequest var requestBody = new OpenAiRequest
{ {
Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model, Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model,
Temperature = 0.0, Temperature = 0.0,
ReasoningEffort = _reasoningEffort, ReasoningEffort = _reasoningEffort,
Messages = new[] Messages =
{ [
new OpenAiRequestMessage { Role = "system", Content = systemPrompt }, new OpenAiRequestMessage { Role = "system", Content = systemPrompt },
new OpenAiRequestMessage { Role = "user", Content = $"<transcript>{rawTranscript}</transcript>" } new OpenAiRequestMessage { Role = "user", Content = $"<transcript>{rawTranscript}</transcript>" }
} ]
}; };
var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json"); var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json");
@@ -89,19 +87,19 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient
return result?.Choices?.FirstOrDefault()?.Message?.Content ?? string.Empty; return result?.Choices?.FirstOrDefault()?.Message?.Content ?? string.Empty;
} }
public async IAsyncEnumerable<string> RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel) public async IAsyncEnumerable<string> RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel)
{ {
var requestBody = new OpenAiRequest var requestBody = new OpenAiRequest
{ {
Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model, Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model,
Temperature = 0.0, Temperature = 0.0,
Stream = true, Stream = true,
ReasoningEffort = _reasoningEffort, ReasoningEffort = _reasoningEffort,
Messages = new[] Messages =
{ [
new OpenAiRequestMessage { Role = "system", Content = systemPrompt }, new OpenAiRequestMessage { Role = "system", Content = systemPrompt },
new OpenAiRequestMessage { Role = "user", Content = $"<transcript>{rawTranscript}</transcript>" } new OpenAiRequestMessage { Role = "user", Content = $"<transcript>{rawTranscript}</transcript>" }
} ]
}; };
var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json"); var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json");
@@ -120,7 +118,7 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient
throw new Exception($"OpenAi API Error: {response.StatusCode} - {error}"); throw new Exception($"OpenAi API Error: {response.StatusCode} - {error}");
} }
using var stream = await response.Content.ReadAsStreamAsync(); await using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
string? line; string? line;

View File

@@ -1,32 +1,24 @@
using System.Diagnostics; using System.Diagnostics;
using Toak.Core; using Toak.Core;
using Toak.IO;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
namespace Toak.Audio; namespace Toak.Audio;
public class AudioRecorder : IAudioRecorder public class AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) : IAudioRecorder
{ {
private readonly string WavPath = Constants.Paths.RecordingWavFile; private readonly string _wavPath = Constants.Paths.RecordingWavFile;
private readonly IRecordingStateTracker _stateTracker; private readonly IRecordingStateTracker _stateTracker = stateTracker;
private readonly INotifications _notifications; private readonly INotifications _notifications = notifications;
public AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) public string GetWavPath() => _wavPath;
{
_stateTracker = stateTracker;
_notifications = notifications;
}
public string GetWavPath() => WavPath;
public void StartRecording() public void StartRecording()
{ {
if (File.Exists(WavPath)) if (File.Exists(_wavPath))
{ {
Logger.LogDebug($"Deleting old audio file: {WavPath}"); Logger.LogDebug($"Deleting old audio file: {_wavPath}");
File.Delete(WavPath); File.Delete(_wavPath);
} }
Logger.LogDebug("Starting pw-record to record audio..."); Logger.LogDebug("Starting pw-record to record audio...");
@@ -34,7 +26,7 @@ public class AudioRecorder : IAudioRecorder
var pInfo = new ProcessStartInfo var pInfo = new ProcessStartInfo
{ {
FileName = Constants.Commands.AudioRecord, FileName = Constants.Commands.AudioRecord,
Arguments = $"--rate=16000 --channels=1 --format=s16 \"{WavPath}\"", Arguments = $"--rate=16000 --channels=1 --format=s16 \"{_wavPath}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,

View File

@@ -1,33 +1,23 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using Toak.Core; using Toak.Core;
using Toak.IO;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
namespace Toak.Audio; namespace Toak.Audio;
public class FfmpegAudioRecorder : IAudioRecorder public class FfmpegAudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) : IAudioRecorder
{ {
private readonly string WavPath = Constants.Paths.RecordingWavFile; private readonly string _wavPath = Constants.Paths.RecordingWavFile;
private readonly IRecordingStateTracker _stateTracker; private readonly IRecordingStateTracker _stateTracker = stateTracker;
private readonly INotifications _notifications; private readonly INotifications _notifications = notifications;
public FfmpegAudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) public string GetWavPath() => _wavPath;
{
_stateTracker = stateTracker;
_notifications = notifications;
}
public string GetWavPath() => WavPath;
public void StartRecording() public void StartRecording()
{ {
if (File.Exists(WavPath)) if (File.Exists(_wavPath))
{ {
Logger.LogDebug($"Deleting old audio file: {WavPath}"); Logger.LogDebug($"Deleting old audio file: {_wavPath}");
File.Delete(WavPath); File.Delete(_wavPath);
} }
Logger.LogDebug("Starting ffmpeg to record audio..."); Logger.LogDebug("Starting ffmpeg to record audio...");
@@ -35,7 +25,7 @@ public class FfmpegAudioRecorder : IAudioRecorder
var pInfo = new ProcessStartInfo var pInfo = new ProcessStartInfo
{ {
FileName = Constants.Commands.AudioFfmpeg, FileName = Constants.Commands.AudioFfmpeg,
Arguments = $"-f pulse -i default -ac 1 -ar 16000 \"{WavPath}\"", Arguments = $"-f pulse -i default -ac 1 -ar 16000 \"{_wavPath}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
@@ -53,14 +43,12 @@ public class FfmpegAudioRecorder : IAudioRecorder
public void StopRecording() public void StopRecording()
{ {
var pid = _stateTracker.GetRecordingPid(); var pid = _stateTracker.GetRecordingPid();
if (pid.HasValue) if (!pid.HasValue) return;
{
Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop..."); Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop...");
try try
{ {
var process = Process.GetProcessById(pid.Value); var process = Process.GetProcessById(pid.Value);
if (!process.HasExited) if (process.HasExited) return;
{
// Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly // Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly
Process.Start(new ProcessStartInfo Process.Start(new ProcessStartInfo
{ {
@@ -72,7 +60,6 @@ public class FfmpegAudioRecorder : IAudioRecorder
process.WaitForExit(2000); // give it a moment to flush process.WaitForExit(2000); // give it a moment to flush
} }
}
catch (Exception ex) catch (Exception ex)
{ {
// Process might already be dead // Process might already be dead
@@ -84,4 +71,3 @@ public class FfmpegAudioRecorder : IAudioRecorder
} }
} }
} }
}

View File

@@ -1,4 +1,3 @@
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Configuration; using Toak.Configuration;
@@ -6,9 +5,9 @@ namespace Toak.Commands;
public static class ConfigUpdaterCommand public static class ConfigUpdaterCommand
{ {
public static async Task ExecuteAsync(string key, string val, bool verbose) public static Task ExecuteAsync(string key, string val, bool verbose)
{ {
Toak.Core.Logger.Verbose = verbose; Core.Logger.Verbose = verbose;
var configManager = new ConfigManager(); var configManager = new ConfigManager();
var config = configManager.LoadConfig(); var config = configManager.LoadConfig();
key = key.ToLowerInvariant(); key = key.ToLowerInvariant();
@@ -23,18 +22,19 @@ public static class ConfigUpdaterCommand
case "backend": config.TypingBackend = val; break; case "backend": config.TypingBackend = val; break;
case "punctuation": case "punctuation":
if (bool.TryParse(val, out var p)) { config.ModulePunctuation = p; } if (bool.TryParse(val, out var p)) { config.ModulePunctuation = p; }
else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return; } else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return Task.CompletedTask; }
break; break;
case "tech": case "tech":
if (bool.TryParse(val, out var t)) { config.ModuleTechnicalSanitization = t; } if (bool.TryParse(val, out var t)) { config.ModuleTechnicalSanitization = t; }
else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return; } else { AnsiConsole.MarkupLine("[red]Invalid value. Use true or false.[/]"); return Task.CompletedTask; }
break; break;
default: default:
AnsiConsole.MarkupLine($"[red]Unknown config key: {key}[/]"); AnsiConsole.MarkupLine($"[red]Unknown config key: {key}[/]");
return; return Task.CompletedTask;
} }
configManager.SaveConfig(config); configManager.SaveConfig(config);
AnsiConsole.MarkupLine($"[green]Successfully[/] set {key} to [blue]{val}[/]."); AnsiConsole.MarkupLine($"[green]Successfully[/] set {key} to [blue]{val}[/].");
return Task.CompletedTask;
} }
} }

View File

@@ -1,6 +1,4 @@
using System;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;

View File

@@ -1,9 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.CommandLine;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;
@@ -54,15 +48,15 @@ public static class HistoryCommand
{ {
try try
{ {
using var writer = new StreamWriter(export); await using var writer = new StreamWriter(export);
writer.WriteLine($"# Toak Transcriptions - {DateTime.Now:yyyy-MM-dd}"); await writer.WriteLineAsync($"# Toak Transcriptions - {DateTime.Now:yyyy-MM-dd}");
writer.WriteLine(); await writer.WriteLineAsync();
foreach (var entry in entries) foreach (var entry in entries)
{ {
writer.WriteLine($"## {entry.Timestamp.ToLocalTime():HH:mm:ss}"); await writer.WriteLineAsync($"## {entry.Timestamp.ToLocalTime():HH:mm:ss}");
writer.WriteLine(entry.RefinedText); await writer.WriteLineAsync(entry.RefinedText);
writer.WriteLine(); await writer.WriteLineAsync();
} }
AnsiConsole.MarkupLine($"[green]Successfully exported {entries.Count} entries to {export}[/]"); AnsiConsole.MarkupLine($"[green]Successfully exported {entries.Count} entries to {export}[/]");

View File

@@ -1,7 +1,4 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Api; using Toak.Api;
using Toak.Configuration; using Toak.Configuration;
@@ -34,7 +31,7 @@ public static class LatencyTestCommand
RedirectStandardOutput = true RedirectStandardOutput = true
}; };
var proc = Process.Start(pInfo); var proc = Process.Start(pInfo);
proc?.WaitForExit(); if (proc != null) await proc.WaitForExitAsync();
if (!File.Exists(testWavPath)) if (!File.Exists(testWavPath))
{ {
@@ -51,13 +48,13 @@ public static class LatencyTestCommand
{ {
ctx.Status("Testing STT (Whisper)..."); ctx.Status("Testing STT (Whisper)...");
var sttWatch = Stopwatch.StartNew(); var sttWatch = Stopwatch.StartNew();
var transcript = await client.TranscribeAsync(testWavPath, config.WhisperLanguage, config.WhisperModel); await client.TranscribeAsync(testWavPath, config.WhisperLanguage, config.WhisperModel);
sttWatch.Stop(); sttWatch.Stop();
ctx.Status("Testing LLM (Llama)..."); ctx.Status("Testing LLM (Llama)...");
var systemPrompt = PromptBuilder.BuildPrompt(config); var systemPrompt = PromptBuilder.BuildPrompt(config);
var llmWatch = Stopwatch.StartNew(); var llmWatch = Stopwatch.StartNew();
var refinedText = await client.RefineTextAsync("Hello world, this is a latency test.", systemPrompt, config.LlmModel); await client.RefineTextAsync("Hello world, this is a latency test.", systemPrompt, config.LlmModel);
llmWatch.Stop(); llmWatch.Stop();
var total = sttWatch.ElapsedMilliseconds + llmWatch.ElapsedMilliseconds; var total = sttWatch.ElapsedMilliseconds + llmWatch.ElapsedMilliseconds;
@@ -73,14 +70,9 @@ public static class LatencyTestCommand
AnsiConsole.Write(table); AnsiConsole.Write(table);
if (total < 1500) AnsiConsole.MarkupLine(total < 1500
{ ? $"[green]Status: OK (under 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]"
AnsiConsole.MarkupLine($"[green]Status: OK (under 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]"); : $"[yellow]Status: SLOW (over 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]");
}
else
{
AnsiConsole.MarkupLine($"[yellow]Status: SLOW (over 1.5s target). Total time: {(total / 1000.0):0.0}s.[/]");
}
}); });
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,7 +1,4 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Configuration; using Toak.Configuration;
using Toak.Core.Skills; using Toak.Core.Skills;
@@ -12,7 +9,7 @@ public static class OnboardCommand
{ {
public static async Task ExecuteAsync(bool verbose) public static async Task ExecuteAsync(bool verbose)
{ {
Toak.Core.Logger.Verbose = verbose; Core.Logger.Verbose = verbose;
var configManager = new ConfigManager(); var configManager = new ConfigManager();
var config = configManager.LoadConfig(); var config = configManager.LoadConfig();
@@ -121,7 +118,7 @@ public static class OnboardCommand
} }
catch (Exception ex) catch (Exception ex)
{ {
Toak.Core.Logger.LogDebug($"Failed to restart toak service: {ex.Message}"); Core.Logger.LogDebug($"Failed to restart toak service: {ex.Message}");
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Configuration; using Toak.Configuration;
@@ -8,7 +7,7 @@ public static class ShowCommand
{ {
public static async Task ExecuteAsync(bool verbose) public static async Task ExecuteAsync(bool verbose)
{ {
Toak.Core.Logger.Verbose = verbose; Core.Logger.Verbose = verbose;
var config = new ConfigManager().LoadConfig(); var config = new ConfigManager().LoadConfig();
var table = new Table(); var table = new Table();

View File

@@ -1,8 +1,5 @@
using System;
using System.CommandLine; using System.CommandLine;
using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Core.Skills; using Toak.Core.Skills;
using Toak.Serialization; using Toak.Serialization;
@@ -100,8 +97,8 @@ public static class SkillCommand
}; };
SkillRegistry.Initialize(); // ensure dir exists SkillRegistry.Initialize(); // ensure dir exists
string filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json"); var filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json");
string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); var json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
File.WriteAllText(filename, json); File.WriteAllText(filename, json);
AnsiConsole.MarkupLine($"[bold green]Success![/] Skill '{name}' saved to {filename}"); AnsiConsole.MarkupLine($"[bold green]Success![/] Skill '{name}' saved to {filename}");

View File

@@ -1,6 +1,4 @@
using System;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;

View File

@@ -1,7 +1,3 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.CommandLine;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;
@@ -9,7 +5,7 @@ namespace Toak.Commands;
public static class StatsCommand public static class StatsCommand
{ {
public static async Task ExecuteAsync(bool verbose) public static Task ExecuteAsync(bool verbose)
{ {
Logger.Verbose = verbose; Logger.Verbose = verbose;
@@ -17,7 +13,7 @@ public static class StatsCommand
if (entries.Count == 0) if (entries.Count == 0)
{ {
AnsiConsole.MarkupLine("[yellow]No history found. Cannot generate statistics.[/]"); AnsiConsole.MarkupLine("[yellow]No history found. Cannot generate statistics.[/]");
return; return Task.CompletedTask;
} }
var totalCount = entries.Count; var totalCount = entries.Count;
@@ -30,7 +26,7 @@ public static class StatsCommand
.FirstOrDefault(); .FirstOrDefault();
var topWords = entries var topWords = entries
.SelectMany(e => e.RefinedText.Split(new[] { ' ', '.', ',', '!', '?' }, StringSplitOptions.RemoveEmptyEntries)) .SelectMany(e => e.RefinedText.Split([' ', '.', ',', '!', '?'], StringSplitOptions.RemoveEmptyEntries))
.Where(w => w.Length > 3) // Exclude short common words .Where(w => w.Length > 3) // Exclude short common words
.GroupBy(w => w.ToLowerInvariant()) .GroupBy(w => w.ToLowerInvariant())
.OrderByDescending(g => g.Count()) .OrderByDescending(g => g.Count())
@@ -52,5 +48,6 @@ public static class StatsCommand
{ {
AnsiConsole.MarkupLine($"[dim]Top spoken words (>3 chars):[/] {string.Join(", ", topWords)}"); AnsiConsole.MarkupLine($"[dim]Top spoken words (>3 chars):[/] {string.Join(", ", topWords)}");
} }
return Task.CompletedTask;
} }
} }

View File

@@ -1,6 +1,4 @@
using System;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;
@@ -24,7 +22,7 @@ public static class StatusCommand
await socket.SendAsync(msg, SocketFlags.None); await socket.SendAsync(msg, SocketFlags.None);
var responseBuffer = new byte[4096]; var responseBuffer = new byte[4096];
int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
if (received > 0) if (received > 0)
{ {
var text = System.Text.Encoding.UTF8.GetString(responseBuffer, 0, received); var text = System.Text.Encoding.UTF8.GetString(responseBuffer, 0, received);
@@ -33,10 +31,7 @@ public static class StatusCommand
} }
catch (SocketException) catch (SocketException)
{ {
if (json) Console.WriteLine(json ? "{\"state\": \"Offline\"}" : "Offline");
Console.WriteLine("{\"state\": \"Offline\"}");
else
Console.WriteLine("Offline");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -1,6 +1,4 @@
using System;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;
@@ -25,7 +23,7 @@ public static class StopCommand
var responseBuffer = new byte[4096]; var responseBuffer = new byte[4096];
while (true) while (true)
{ {
int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
if (received == 0) break; if (received == 0) break;
if (pipeToStdout) if (pipeToStdout)
{ {

View File

@@ -1,6 +1,4 @@
using System;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Spectre.Console; using Spectre.Console;
using Toak.Core; using Toak.Core;
@@ -34,7 +32,7 @@ public static class ToggleCommand
var responseBuffer = new byte[4096]; var responseBuffer = new byte[4096];
while (true) while (true)
{ {
int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None); var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
if (received == 0) break; // socket closed by daemon if (received == 0) break; // socket closed by daemon
if (pipeToStdout) if (pipeToStdout)

View File

@@ -1,6 +1,4 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Toak.Core; using Toak.Core;
using Toak.Serialization; using Toak.Serialization;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
@@ -9,23 +7,19 @@ namespace Toak.Configuration;
public class ConfigManager : IConfigProvider public class ConfigManager : IConfigProvider
{ {
private readonly string ConfigDir = Constants.Paths.ConfigDir; private readonly string _configDir = Constants.Paths.ConfigDir;
private readonly string ConfigPath = Constants.Paths.ConfigFile; private readonly string _configPath = Constants.Paths.ConfigFile;
public ConfigManager()
{
}
public ToakConfig LoadConfig() public ToakConfig LoadConfig()
{ {
if (!File.Exists(ConfigPath)) if (!File.Exists(_configPath))
{ {
return new ToakConfig(); return new ToakConfig();
} }
try try
{ {
var json = File.ReadAllText(ConfigPath); var json = File.ReadAllText(_configPath);
return JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.ToakConfig) ?? new ToakConfig(); return JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.ToakConfig) ?? new ToakConfig();
} }
catch (Exception) catch (Exception)
@@ -36,12 +30,12 @@ public class ConfigManager : IConfigProvider
public void SaveConfig(ToakConfig config) public void SaveConfig(ToakConfig config)
{ {
if (!Directory.Exists(ConfigDir)) if (!Directory.Exists(_configDir))
{ {
Directory.CreateDirectory(ConfigDir); Directory.CreateDirectory(_configDir);
} }
var json = JsonSerializer.Serialize(config, AppJsonSerializerContext.Default.ToakConfig); var json = JsonSerializer.Serialize(config, AppJsonSerializerContext.Default.ToakConfig);
File.WriteAllText(ConfigPath, json); File.WriteAllText(_configPath, json);
} }
} }

View File

@@ -12,10 +12,10 @@ public class ToakConfig
public int MinRecordingDuration { get; set; } = 500; public int MinRecordingDuration { get; set; } = 500;
public string WhisperLanguage { get; set; } = string.Empty; public string WhisperLanguage { get; set; } = string.Empty;
public string LlmModel { get; set; } = Toak.Core.Constants.Defaults.LlmModel; public string LlmModel { get; set; } = Core.Constants.Defaults.LlmModel;
public string ReasoningEffort { get; set; } = "none"; // none or low public string ReasoningEffort { get; set; } = "none"; // none or low
public string WhisperModel { get; set; } = Toak.Core.Constants.Defaults.WhisperModel; public string WhisperModel { get; set; } = Core.Constants.Defaults.WhisperModel;
public string StartSoundPath { get; set; } = "Assets/Audio/beep.wav"; public string StartSoundPath { get; set; } = "Assets/Audio/beep.wav";
public string StopSoundPath { get; set; } = "Assets/Audio/beep.wav"; public string StopSoundPath { get; set; } = "Assets/Audio/beep.wav";
public List<string> ActiveSkills { get; set; } = new List<string> { "Terminal", "Translate" }; public List<string> ActiveSkills { get; set; } = ["Terminal", "Translate"];
} }

View File

@@ -1,6 +1,3 @@
using System;
using System.IO;
namespace Toak.Core; namespace Toak.Core;
public static class Constants public static class Constants

View File

@@ -1,8 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Toak.Configuration; using Toak.Configuration;
using Toak.Api; using Toak.Api;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
@@ -54,15 +50,9 @@ public static class DaemonService
var notifications = new Notifications(); var notifications = new Notifications();
var speechClient = new OpenAiCompatibleClient(config.GroqApiKey); var speechClient = new OpenAiCompatibleClient(config.GroqApiKey);
ILlmClient llmClient; ILlmClient llmClient = config.LlmProvider == "together"
if (config.LlmProvider == "together") ? new OpenAiCompatibleClient(config.TogetherApiKey, "https://api.together.xyz/v1/", config.ReasoningEffort)
{ : new OpenAiCompatibleClient(config.GroqApiKey, "https://api.groq.com/openai/v1/", config.ReasoningEffort);
llmClient = new OpenAiCompatibleClient(config.TogetherApiKey, "https://api.together.xyz/v1/", config.ReasoningEffort);
}
else
{
llmClient = new OpenAiCompatibleClient(config.GroqApiKey, "https://api.groq.com/openai/v1/", config.ReasoningEffort);
}
IAudioRecorder recorder = config.AudioBackend == "ffmpeg" IAudioRecorder recorder = config.AudioBackend == "ffmpeg"
? new FfmpegAudioRecorder(stateTracker, notifications) ? new FfmpegAudioRecorder(stateTracker, notifications)
@@ -114,12 +104,12 @@ public static class DaemonService
try try
{ {
var buffer = new byte[3]; var buffer = new byte[3];
int bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None); var bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None);
if (bytesRead > 0) if (bytesRead > 0)
{ {
byte cmd = buffer[0]; var cmd = buffer[0];
bool pipeToStdout = bytesRead > 1 && buffer[1] == 1; var pipeToStdout = bytesRead > 1 && buffer[1] == 1;
bool copyToClipboard = bytesRead > 2 && buffer[2] == 1; var copyToClipboard = bytesRead > 2 && buffer[2] == 1;
if (cmd == 1) // START if (cmd == 1) // START
{ {
@@ -142,9 +132,9 @@ public static class DaemonService
} }
else if (cmd == 5) // STATUS else if (cmd == 5) // STATUS
{ {
bool json = pipeToStdout; // buffer[1] == 1 is json var json = pipeToStdout; // buffer[1] == 1 is json
bool isRecording = stateTracker.IsRecording(); var isRecording = stateTracker.IsRecording();
string stateStr = isRecording ? "Recording" : "Idle"; var stateStr = isRecording ? "Recording" : "Idle";
if (json) if (json)
{ {

View File

@@ -1,5 +1,3 @@
using System;
namespace Toak.Core; namespace Toak.Core;
public class HistoryEntry public class HistoryEntry

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Toak.Serialization; using Toak.Serialization;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
@@ -10,20 +6,16 @@ namespace Toak.Core;
public class HistoryManager : IHistoryManager public class HistoryManager : IHistoryManager
{ {
private readonly string HistoryDir = Constants.Paths.AppDataDir; private readonly string _historyDir = Constants.Paths.AppDataDir;
private readonly string HistoryFile = Constants.Paths.HistoryFile; private readonly string _historyFile = Constants.Paths.HistoryFile;
public HistoryManager()
{
}
public void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs) public void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs)
{ {
try try
{ {
if (!Directory.Exists(HistoryDir)) if (!Directory.Exists(_historyDir))
{ {
Directory.CreateDirectory(HistoryDir); Directory.CreateDirectory(_historyDir);
} }
var entry = new HistoryEntry var entry = new HistoryEntry
@@ -38,9 +30,9 @@ public class HistoryManager : IHistoryManager
var json = JsonSerializer.Serialize(entry, CompactJsonSerializerContext.Default.HistoryEntry); var json = JsonSerializer.Serialize(entry, CompactJsonSerializerContext.Default.HistoryEntry);
// Thread-safe append // Thread-safe append
lock (HistoryFile) lock (_historyFile)
{ {
File.AppendAllLines(HistoryFile, new[] { json }); File.AppendAllLines(_historyFile, [json]);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -52,14 +44,14 @@ public class HistoryManager : IHistoryManager
public List<HistoryEntry> LoadHistory() public List<HistoryEntry> LoadHistory()
{ {
var entries = new List<HistoryEntry>(); var entries = new List<HistoryEntry>();
if (!File.Exists(HistoryFile)) return entries; if (!File.Exists(_historyFile)) return entries;
try try
{ {
string[] lines; string[] lines;
lock (HistoryFile) lock (_historyFile)
{ {
lines = File.ReadAllLines(HistoryFile); lines = File.ReadAllLines(_historyFile);
} }
foreach (var line in lines) foreach (var line in lines)
@@ -92,20 +84,20 @@ public class HistoryManager : IHistoryManager
public void ClearHistory() public void ClearHistory()
{ {
if (File.Exists(HistoryFile)) if (File.Exists(_historyFile))
{ {
try try
{ {
lock (HistoryFile) lock (_historyFile)
{ {
// Securely delete // Securely delete
var len = new FileInfo(HistoryFile).Length; var len = new FileInfo(_historyFile).Length;
using (var fs = new FileStream(HistoryFile, FileMode.Open, FileAccess.Write)) using (var fs = new FileStream(_historyFile, FileMode.Open, FileAccess.Write))
{ {
var blank = new byte[len]; var blank = new byte[len];
fs.Write(blank, 0, blank.Length); fs.Write(blank, 0, blank.Length);
} }
File.Delete(HistoryFile); File.Delete(_historyFile);
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,5 +1,4 @@
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
namespace Toak.Core.Interfaces; namespace Toak.Core.Interfaces;

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Toak.Configuration; using Toak.Configuration;
namespace Toak.Core.Interfaces; namespace Toak.Core.Interfaces;
@@ -13,13 +10,13 @@ public interface IConfigProvider
public interface ISpeechClient public interface ISpeechClient
{ {
Task<string> TranscribeAsync(string filePath, string language = "", string model = Toak.Core.Constants.Defaults.WhisperModel); Task<string> TranscribeAsync(string filePath, string language = "", string model = Constants.Defaults.WhisperModel);
} }
public interface ILlmClient public interface ILlmClient
{ {
Task<string> RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel); Task<string> RefineTextAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel);
IAsyncEnumerable<string> RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel); IAsyncEnumerable<string> RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel);
} }
public interface IAudioRecorder public interface IAudioRecorder

View File

@@ -1,5 +1,4 @@
using System.Text; using System.Text;
using Toak.Configuration; using Toak.Configuration;
namespace Toak.Core; namespace Toak.Core;

View File

@@ -1,11 +1,10 @@
using System;
using System.Diagnostics; using System.Diagnostics;
namespace Toak.Core.Skills; namespace Toak.Core.Skills;
public class DynamicSkill : ISkill public class DynamicSkill(SkillDefinition def) : ISkill
{ {
private readonly SkillDefinition _def; private readonly SkillDefinition _def = def;
public string Name => _def.Name; public string Name => _def.Name;
public string Description => _def.Description; public string Description => _def.Description;
@@ -13,11 +12,6 @@ public class DynamicSkill : ISkill
public bool HandlesExecution => _def.Action.ToLowerInvariant() == "script"; public bool HandlesExecution => _def.Action.ToLowerInvariant() == "script";
public DynamicSkill(SkillDefinition def)
{
_def = def;
}
public string GetSystemPrompt(string rawTranscript) public string GetSystemPrompt(string rawTranscript)
{ {
return _def.SystemPrompt.Replace("{transcript}", rawTranscript); return _def.SystemPrompt.Replace("{transcript}", rawTranscript);

View File

@@ -4,7 +4,7 @@ public class SkillDefinition
{ {
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public string[] Hotwords { get; set; } = System.Array.Empty<string>(); public string[] Hotwords { get; set; } = [];
public string Action { get; set; } = "type"; // "type" or "script" public string Action { get; set; } = "type"; // "type" or "script"
public string SystemPrompt { get; set; } = ""; public string SystemPrompt { get; set; } = "";
public string? ScriptPath { get; set; } public string? ScriptPath { get; set; }

View File

@@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using Toak.Serialization; using Toak.Serialization;
@@ -9,7 +5,7 @@ namespace Toak.Core.Skills;
public static class SkillRegistry public static class SkillRegistry
{ {
public static List<ISkill> AllSkills = new List<ISkill>(); public static List<ISkill> AllSkills = [];
public static string SkillsDirectory => Path.Combine( public static string SkillsDirectory => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
@@ -28,7 +24,7 @@ public static class SkillRegistry
{ {
try try
{ {
string json = File.ReadAllText(file); var json = File.ReadAllText(file);
var def = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillDefinition); var def = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillDefinition);
if (def != null) if (def != null)
{ {
@@ -47,7 +43,7 @@ public static class SkillRegistry
if (AllSkills.Count == 0) Initialize(); if (AllSkills.Count == 0) Initialize();
var activeSkills = AllSkills.Where(s => activeSkillNames.Contains(s.Name, StringComparer.OrdinalIgnoreCase)).ToList(); var activeSkills = AllSkills.Where(s => activeSkillNames.Contains(s.Name, StringComparer.OrdinalIgnoreCase)).ToList();
string normalizedTranscript = transcript.Trim(); var normalizedTranscript = transcript.Trim();
foreach (var skill in activeSkills) foreach (var skill in activeSkills)
{ {
@@ -72,9 +68,11 @@ public static class SkillRegistry
Description = "Translates the spoken command into a bash command and types it.", Description = "Translates the spoken command into a bash command and types it.",
Hotwords = ["System terminal", "System run", "System execute"], Hotwords = ["System terminal", "System run", "System execute"],
Action = "type", Action = "type",
SystemPrompt = @"You are a Linux terminal expert. SystemPrompt = """
You are a Linux terminal expert.
Translate the user's request into a single, valid bash command. Translate the user's request into a single, valid bash command.
Output ONLY the raw command, no formatting, no markdown." Output ONLY the raw command, no formatting, no markdown.
"""
}, },
new SkillDefinition new SkillDefinition
{ {
@@ -82,10 +80,12 @@ Output ONLY the raw command, no formatting, no markdown."
Description = "Translates the spoken text into another language on the fly.", Description = "Translates the spoken text into another language on the fly.",
Hotwords = ["System translate to", "System translate into"], Hotwords = ["System translate to", "System translate into"],
Action = "type", Action = "type",
SystemPrompt = @"You are an expert translator. The user wants to translate the following text. SystemPrompt = """
You are an expert translator. The user wants to translate the following text.
The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:'). The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:').
Translate the REST of the transcript into that target language. Translate the REST of the transcript into that target language.
Output ONLY the final translated text. Do not include markdown, explanations, or quotes." Output ONLY the final translated text. Do not include markdown, explanations, or quotes.
"""
}, },
new SkillDefinition new SkillDefinition
{ {
@@ -93,13 +93,15 @@ Output ONLY the final translated text. Do not include markdown, explanations, or
Description = "Rewrites text into a formal, articulate tone.", Description = "Rewrites text into a formal, articulate tone.",
Hotwords = ["System professional", "System formalize", "System formal"], Hotwords = ["System professional", "System formalize", "System formal"],
Action = "type", Action = "type",
SystemPrompt = @"Rewrite the following text to be articulate and formal. SystemPrompt = """
Rewrite the following text to be articulate and formal.
The text will start with 'System professional', 'System formalize', or 'System formal', The text will start with 'System professional', 'System formalize', or 'System formal',
or something along the lines of that. You can ignore those words. or something along the lines of that. You can ignore those words.
Do not add any conversational filler. Do not add any conversational filler.
Make sure to preserve the meaning of the original text. Make sure to preserve the meaning of the original text.
Output ONLY the final professional text. Output ONLY the final professional text.
Text: {transcript}" Text: {transcript}
"""
}, },
new SkillDefinition new SkillDefinition
{ {
@@ -107,19 +109,21 @@ Text: {transcript}"
Description = "Provides a direct, crisp summary of the dictation.", Description = "Provides a direct, crisp summary of the dictation.",
Hotwords = ["System summary", "System concise", "System summarize"], Hotwords = ["System summary", "System concise", "System summarize"],
Action = "type", Action = "type",
SystemPrompt = @"Summarize the following text to be as concise SystemPrompt = """
Summarize the following text to be as concise
and direct as possible. and direct as possible.
The text will start with 'System summary', 'System concise', or 'System summarize', The text will start with 'System summary', 'System concise', or 'System summarize',
and you shoul ignore that part of the text. and you shoul ignore that part of the text.
Output ONLY the final summary text. Output ONLY the final summary text.
Text: {transcript}" Text: {transcript}
"""
} }
}; };
foreach (var def in defaults) foreach (var def in defaults)
{ {
string filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json"); var filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json");
string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); var json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
File.WriteAllText(filename, json); File.WriteAllText(filename, json);
} }

View File

@@ -4,24 +4,24 @@ namespace Toak.Core;
public class StateTracker : IRecordingStateTracker public class StateTracker : IRecordingStateTracker
{ {
private readonly string StateFilePath = Constants.Paths.StateFile; private readonly string _stateFilePath = Constants.Paths.StateFile;
public bool IsRecording() public bool IsRecording()
{ {
return File.Exists(StateFilePath); return File.Exists(_stateFilePath);
} }
public void SetRecording(int ffmpegPid) public void SetRecording(int ffmpegPid)
{ {
Logger.LogDebug($"Setting recording state with PID {ffmpegPid}"); Logger.LogDebug($"Setting recording state with PID {ffmpegPid}");
File.WriteAllText(StateFilePath, $"{ffmpegPid}\n{DateTime.UtcNow.Ticks}"); File.WriteAllText(_stateFilePath, $"{ffmpegPid}\n{DateTime.UtcNow.Ticks}");
} }
public int? GetRecordingPid() public int? GetRecordingPid()
{ {
if (File.Exists(StateFilePath)) if (File.Exists(_stateFilePath))
{ {
var lines = File.ReadAllLines(StateFilePath); var lines = File.ReadAllLines(_stateFilePath);
if (lines.Length > 0 && int.TryParse(lines[0], out var pid)) if (lines.Length > 0 && int.TryParse(lines[0], out var pid))
{ {
Logger.LogDebug($"Read recording PID {pid} from state file"); Logger.LogDebug($"Read recording PID {pid} from state file");
@@ -33,9 +33,9 @@ public class StateTracker : IRecordingStateTracker
public DateTime? GetRecordingStartTime() public DateTime? GetRecordingStartTime()
{ {
if (File.Exists(StateFilePath)) if (File.Exists(_stateFilePath))
{ {
var lines = File.ReadAllLines(StateFilePath); var lines = File.ReadAllLines(_stateFilePath);
if (lines.Length > 1 && long.TryParse(lines[1], out var ticks)) if (lines.Length > 1 && long.TryParse(lines[1], out var ticks))
{ {
return new DateTime(ticks, DateTimeKind.Utc); return new DateTime(ticks, DateTimeKind.Utc);
@@ -46,10 +46,10 @@ public class StateTracker : IRecordingStateTracker
public void ClearRecording() public void ClearRecording()
{ {
if (File.Exists(StateFilePath)) if (File.Exists(_stateFilePath))
{ {
Logger.LogDebug("Clearing recording state file"); Logger.LogDebug("Clearing recording state file");
File.Delete(StateFilePath); File.Delete(_stateFilePath);
} }
} }
} }

View File

@@ -1,26 +1,10 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
using Toak.Configuration;
namespace Toak.Core; namespace Toak.Core;
public class TranscriptionOrchestrator : ITranscriptionOrchestrator public class TranscriptionOrchestrator(
{
private readonly ISpeechClient _speechClient;
private readonly ILlmClient _llmClient;
private readonly IConfigProvider _configProvider;
private readonly IAudioRecorder _audioRecorder;
private readonly INotifications _notifications;
private readonly ITextInjector _textInjector;
private readonly IHistoryManager _historyManager;
private readonly IClipboardManager _clipboardManager;
private readonly IRecordingStateTracker _stateTracker;
public TranscriptionOrchestrator(
ISpeechClient speechClient, ISpeechClient speechClient,
ILlmClient llmClient, ILlmClient llmClient,
IConfigProvider configProvider, IConfigProvider configProvider,
@@ -29,27 +13,27 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
ITextInjector textInjector, ITextInjector textInjector,
IHistoryManager historyManager, IHistoryManager historyManager,
IClipboardManager clipboardManager, IClipboardManager clipboardManager,
IRecordingStateTracker stateTracker) IRecordingStateTracker stateTracker) : ITranscriptionOrchestrator
{ {
_speechClient = speechClient; private readonly ISpeechClient _speechClient = speechClient;
_llmClient = llmClient; private readonly ILlmClient _llmClient = llmClient;
_configProvider = configProvider; private readonly IConfigProvider _configProvider = configProvider;
_audioRecorder = audioRecorder; private readonly IAudioRecorder _audioRecorder = audioRecorder;
_notifications = notifications; private readonly INotifications _notifications = notifications;
_textInjector = textInjector; private readonly ITextInjector _textInjector = textInjector;
_historyManager = historyManager; private readonly IHistoryManager _historyManager = historyManager;
_clipboardManager = clipboardManager; private readonly IClipboardManager _clipboardManager = clipboardManager;
_stateTracker = stateTracker; private readonly IRecordingStateTracker _stateTracker = stateTracker;
}
public async Task ProcessStartRecordingAsync() public Task ProcessStartRecordingAsync()
{ {
if (_stateTracker.IsRecording()) return; if (_stateTracker.IsRecording()) return Task.CompletedTask;
Logger.LogDebug("Received START command"); Logger.LogDebug("Received START command");
var config = _configProvider.LoadConfig(); var config = _configProvider.LoadConfig();
_notifications.PlaySound(config.StartSoundPath); _notifications.PlaySound(config.StartSoundPath);
_audioRecorder.StartRecording(); _audioRecorder.StartRecording();
return Task.CompletedTask;
} }
public async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard) public async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard)
@@ -96,9 +80,9 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
return; return;
} }
var detectedSkill = Toak.Core.Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills); var detectedSkill = Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills);
string systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config); var systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config);
bool isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution; var isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution;
if (isExecutionSkill) if (isExecutionSkill)
{ {
@@ -118,7 +102,7 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
if (pipeToStdout || copyToClipboard) if (pipeToStdout || copyToClipboard)
{ {
string fullText = ""; var fullText = "";
await foreach (var token in tokenStream) await foreach (var token in tokenStream)
{ {
fullText += token; fullText += token;
@@ -137,7 +121,7 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
} }
else else
{ {
string fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend); var fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend);
stopWatch.Stop(); stopWatch.Stop();
_historyManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds); _historyManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds);
_notifications.Notify("Toak", $"Done in {stopWatch.ElapsedMilliseconds}ms"); _notifications.Notify("Toak", $"Done in {stopWatch.ElapsedMilliseconds}ms");

View File

@@ -1,31 +1,25 @@
using System.Diagnostics; using System.Diagnostics;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
namespace Toak.IO; namespace Toak.IO;
public class ClipboardManager : IClipboardManager public class ClipboardManager(INotifications notifications) : IClipboardManager
{ {
private readonly INotifications _notifications; private readonly INotifications _notifications = notifications;
public ClipboardManager(INotifications notifications)
{
_notifications = notifications;
}
public void Copy(string text) public void Copy(string text)
{ {
if (string.IsNullOrWhiteSpace(text)) return; if (string.IsNullOrWhiteSpace(text)) return;
try try
{ {
string sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLowerInvariant() ?? ""; var sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLowerInvariant() ?? "";
ProcessStartInfo pInfo; ProcessStartInfo pInfo;
if (sessionType == "wayland") if (sessionType == "wayland")
{ {
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.ClipboardWayland, FileName = Core.Constants.Commands.ClipboardWayland,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardInput = true RedirectStandardInput = true
@@ -35,7 +29,7 @@ public class ClipboardManager : IClipboardManager
{ {
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.ClipboardX11, FileName = Core.Constants.Commands.ClipboardX11,
Arguments = "-selection clipboard", Arguments = "-selection clipboard",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
@@ -44,15 +38,13 @@ public class ClipboardManager : IClipboardManager
} }
var process = Process.Start(pInfo); var process = Process.Start(pInfo);
if (process != null) if (process == null) return;
{
using (var sw = process.StandardInput) using (var sw = process.StandardInput)
{ {
sw.Write(text); sw.Write(text);
} }
process.WaitForExit(); process.WaitForExit();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[ClipboardManager] Error copying text: {ex.Message}"); Console.WriteLine($"[ClipboardManager] Error copying text: {ex.Message}");

View File

@@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
namespace Toak.IO; namespace Toak.IO;
@@ -16,7 +15,7 @@ public class Notifications : INotifications
{ {
var pInfo = new ProcessStartInfo var pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.Notify, FileName = Core.Constants.Commands.Notify,
Arguments = $"-a \"Toak\" \"{summary}\" \"{body}\"", Arguments = $"-a \"Toak\" \"{summary}\" \"{body}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
@@ -66,7 +65,7 @@ public class Notifications : INotifications
var pInfo = new ProcessStartInfo var pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.PlaySound, FileName = Core.Constants.Commands.PlaySound,
Arguments = $"\"{absolutePath}\"", Arguments = $"\"{absolutePath}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true

View File

@@ -1,19 +1,12 @@
using System.Diagnostics; using System.Diagnostics;
using Toak.Core; using Toak.Core;
using Toak.Core.Interfaces; using Toak.Core.Interfaces;
namespace Toak.IO; namespace Toak.IO;
public class TextInjector : ITextInjector public class TextInjector(INotifications notifications) : ITextInjector
{ {
private readonly INotifications _notifications; private readonly INotifications _notifications = notifications;
public TextInjector(INotifications notifications)
{
_notifications = notifications;
}
public Task InjectTextAsync(string text, string backend = "xdotool") public Task InjectTextAsync(string text, string backend = "xdotool")
{ {
@@ -28,8 +21,8 @@ public class TextInjector : ITextInjector
Logger.LogDebug($"Injecting text using wtype..."); Logger.LogDebug($"Injecting text using wtype...");
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.TypeWayland, FileName = Constants.Commands.TypeWayland,
Arguments = $"-d {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"", Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
}; };
@@ -39,7 +32,7 @@ public class TextInjector : ITextInjector
Logger.LogDebug($"Injecting text using ydotool..."); Logger.LogDebug($"Injecting text using ydotool...");
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.TypeYdotool, FileName = Constants.Commands.TypeYdotool,
Arguments = $"type \"{text.Replace("\"", "\\\"")}\"", Arguments = $"type \"{text.Replace("\"", "\\\"")}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
@@ -50,8 +43,8 @@ public class TextInjector : ITextInjector
Logger.LogDebug($"Injecting text using xdotool..."); Logger.LogDebug($"Injecting text using xdotool...");
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.TypeX11, FileName = Constants.Commands.TypeX11,
Arguments = $"type --clearmodifiers --delay {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"", Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
}; };
@@ -69,7 +62,7 @@ public class TextInjector : ITextInjector
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream, string backend) public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream, string backend)
{ {
string fullText = string.Empty; var fullText = string.Empty;
try try
{ {
ProcessStartInfo pInfo; ProcessStartInfo pInfo;
@@ -78,8 +71,8 @@ public class TextInjector : ITextInjector
Logger.LogDebug($"Setting up stream injection using wtype..."); Logger.LogDebug($"Setting up stream injection using wtype...");
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.TypeWayland, FileName = Constants.Commands.TypeWayland,
Arguments = $"-d {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} -", Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} -",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardInput = true RedirectStandardInput = true
@@ -94,7 +87,7 @@ public class TextInjector : ITextInjector
fullText += token; fullText += token;
var chunkInfo = new ProcessStartInfo var chunkInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.TypeYdotool, FileName = Constants.Commands.TypeYdotool,
Arguments = $"type \"{token.Replace("\"", "\\\"")}\"", Arguments = $"type \"{token.Replace("\"", "\\\"")}\"",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
@@ -109,8 +102,8 @@ public class TextInjector : ITextInjector
Logger.LogDebug($"Setting up stream injection using xdotool..."); Logger.LogDebug($"Setting up stream injection using xdotool...");
pInfo = new ProcessStartInfo pInfo = new ProcessStartInfo
{ {
FileName = Toak.Core.Constants.Commands.TypeX11, FileName = Constants.Commands.TypeX11,
Arguments = $"type --clearmodifiers --delay {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} --file -", Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} --file -",
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardInput = true RedirectStandardInput = true

View File

@@ -1,5 +1,4 @@
using System.CommandLine; using System.CommandLine;
using System.Threading.Tasks;
using Toak.Commands; using Toak.Commands;
namespace Toak; namespace Toak;
@@ -44,7 +43,7 @@ public class Program
// Daemon Command // Daemon Command
var daemonCmd = new Command("daemon", "Starts the background service"); var daemonCmd = new Command("daemon", "Starts the background service");
daemonCmd.SetHandler(Toak.Core.DaemonService.StartAsync, verboseOption); daemonCmd.SetHandler(Core.DaemonService.StartAsync, verboseOption);
rootCommand.AddCommand(daemonCmd); rootCommand.AddCommand(daemonCmd);
// Discard Command // Discard Command

View File

@@ -18,14 +18,14 @@ namespace Toak.Serialization;
[JsonSerializable(typeof(OpenAiStreamChoice))] [JsonSerializable(typeof(OpenAiStreamChoice))]
[JsonSerializable(typeof(OpenAiStreamDelta))] [JsonSerializable(typeof(OpenAiStreamDelta))]
[JsonSerializable(typeof(OpenAiStreamChoice[]))] [JsonSerializable(typeof(OpenAiStreamChoice[]))]
[JsonSerializable(typeof(Toak.Core.Skills.SkillDefinition))] [JsonSerializable(typeof(Core.Skills.SkillDefinition))]
[JsonSerializable(typeof(Toak.Core.HistoryEntry))] [JsonSerializable(typeof(Core.HistoryEntry))]
internal partial class AppJsonSerializerContext : JsonSerializerContext internal partial class AppJsonSerializerContext : JsonSerializerContext
{ {
} }
[JsonSourceGenerationOptions(WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSourceGenerationOptions(WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(Toak.Core.HistoryEntry))] [JsonSerializable(typeof(Core.HistoryEntry))]
internal partial class CompactJsonSerializerContext : JsonSerializerContext internal partial class CompactJsonSerializerContext : JsonSerializerContext
{ {
} }

View File

@@ -1,257 +0,0 @@
# Qodana Inspection Results
This document outlines the problems detected in the codebase by the Qodana scan.
## ArrangeObjectCreationWhenTypeEvident
Found 6 occurrences.
- **Configuration/ToakConfig.cs** (Line 20): Redundant type specification [note]
- **Core/Skills/SkillRegistry.cs** (Line 12): Redundant type specification [note]
- **Core/Skills/SkillRegistry.cs** (Line 69): Redundant type specification [note]
- **Core/Skills/SkillRegistry.cs** (Line 79): Redundant type specification [note]
- **Core/Skills/SkillRegistry.cs** (Line 90): Redundant type specification [note]
- **Core/Skills/SkillRegistry.cs** (Line 104): Redundant type specification [note]
## AsyncMethodWithoutAwait
Found 9 occurrences.
- **Commands/ConfigUpdaterCommand.cs** (Line 9): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/HistoryCommand.cs** (Line 14): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/OnboardCommand.cs** (Line 13): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/ShowCommand.cs** (Line 9): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/SkillCommand.cs** (Line 37): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/SkillCommand.cs** (Line 68): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/SkillCommand.cs** (Line 110): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Commands/StatsCommand.cs** (Line 12): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
- **Core/TranscriptionOrchestrator.cs** (Line 45): This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls or drop 'async' to avoid generating state machine for this method [note]
## ClassNeverInstantiated.Global
Found 1 occurrences.
- **Program.cs** (Line 7): Class 'Program' is never instantiated [note]
## ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
Found 4 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 89): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 89): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 135): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 135): Conditional access qualifier expression is never null according to nullable reference types' annotations [warning]
## ConvertClosureToMethodGroup
Found 1 occurrences.
- **Commands/SkillCommand.cs** (Line 31): Convert into method group [note]
## ConvertIfStatementToConditionalTernaryExpression
Found 3 occurrences.
- **Core/DaemonService.cs** (Line 58): Convert into '?:' expression [note]
- **Commands/LatencyTestCommand.cs** (Line 76): Convert into method call with '?:' expression inside [note]
- **Commands/StatusCommand.cs** (Line 36): Convert into method call with '?:' expression inside [note]
## ConvertToPrimaryConstructor
Found 6 occurrences.
- **Audio/AudioRecorder.cs** (Line 16): Convert into primary constructor [note]
- **Audio/FfmpegAudioRecorder.cs** (Line 17): Convert into primary constructor [note]
- **Core/Skills/DynamicSkill.cs** (Line 16): Convert into primary constructor [note]
- **Core/TranscriptionOrchestrator.cs** (Line 23): Convert into primary constructor [note]
- **IO/ClipboardManager.cs** (Line 11): Convert into primary constructor [note]
- **IO/TextInjector.cs** (Line 13): Convert into primary constructor [note]
## EmptyConstructor
Found 2 occurrences.
- **Configuration/ConfigManager.cs** (Line 15): Empty constructor is redundant. The compiler generates the same by default. [warning]
- **Core/HistoryManager.cs** (Line 16): Empty constructor is redundant. The compiler generates the same by default. [warning]
## EmptyGeneralCatchClause
Found 1 occurrences.
- **Core/DaemonService.cs** (Line 42): Empty general catch clause suppresses any errors [warning]
## FieldCanBeMadeReadOnly.Global
Found 1 occurrences.
- **Core/Skills/SkillRegistry.cs** (Line 12): Field can be made readonly [note]
## InconsistentNaming
Found 7 occurrences.
- **Configuration/ConfigManager.cs** (Line 12): Name 'ConfigDir' does not match rule 'Instance fields (private)'. Suggested name is '_configDir'. [warning]
- **Configuration/ConfigManager.cs** (Line 13): Name 'ConfigPath' does not match rule 'Instance fields (private)'. Suggested name is '_configPath'. [warning]
- **Core/HistoryManager.cs** (Line 13): Name 'HistoryDir' does not match rule 'Instance fields (private)'. Suggested name is '_historyDir'. [warning]
- **Core/HistoryManager.cs** (Line 14): Name 'HistoryFile' does not match rule 'Instance fields (private)'. Suggested name is '_historyFile'. [warning]
- **Core/StateTracker.cs** (Line 7): Name 'StateFilePath' does not match rule 'Instance fields (private)'. Suggested name is '_stateFilePath'. [warning]
- **Audio/AudioRecorder.cs** (Line 12): Name 'WavPath' does not match rule 'Instance fields (private)'. Suggested name is '_wavPath'. [warning]
- **Audio/FfmpegAudioRecorder.cs** (Line 13): Name 'WavPath' does not match rule 'Instance fields (private)'. Suggested name is '_wavPath'. [warning]
## MemberCanBePrivate.Global
Found 1 occurrences.
- **Core/Constants.cs** (Line 8): Constant 'AppName' can be made private [note]
## MergeIntoPattern
Found 1 occurrences.
- **Core/TranscriptionOrchestrator.cs** (Line 101): Merge into pattern [note]
## MethodHasAsyncOverload
Found 9 occurrences.
- **Commands/HistoryCommand.cs** (Line 58): Method has async overload [note]
- **Commands/HistoryCommand.cs** (Line 59): Method has async overload [note]
- **Commands/HistoryCommand.cs** (Line 63): Method has async overload [note]
- **Commands/HistoryCommand.cs** (Line 64): Method has async overload [note]
- **Commands/HistoryCommand.cs** (Line 65): Method has async overload [note]
- **Commands/LatencyTestCommand.cs** (Line 37): Method has async overload [note]
- **Commands/OnboardCommand.cs** (Line 119): Method has async overload [note]
- **Commands/SkillCommand.cs** (Line 51): Method has async overload [note]
- **Commands/SkillCommand.cs** (Line 105): Method has async overload [note]
## MoveVariableDeclarationInsideLoopCondition
Found 1 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 125): Variable 'line' can be declared inside loop condition [note]
## NotAccessedField.Local
Found 1 occurrences.
- **Core/DaemonService.cs** (Line 21): Field '_lockFile' is assigned but its value is never used [warning]
## RedundantDefaultMemberInitializer
Found 2 occurrences.
- **Api/Models/OpenAiModels.cs** (Line 20): Initializing property by default value is redundant [warning]
- **Core/Logger.cs** (Line 5): Initializing property by default value is redundant [warning]
## RedundantExplicitParamsArrayCreation
Found 7 occurrences.
- **Commands/OnboardCommand.cs** (Line 31): Redundant explicit collection creation in argument of 'params' parameter [note]
- **Commands/OnboardCommand.cs** (Line 44): Redundant explicit collection creation in argument of 'params' parameter [note]
- **Commands/OnboardCommand.cs** (Line 51): Redundant explicit collection creation in argument of 'params' parameter [note]
- **Commands/OnboardCommand.cs** (Line 60): Redundant explicit collection creation in argument of 'params' parameter [note]
- **Commands/OnboardCommand.cs** (Line 66): Redundant explicit collection creation in argument of 'params' parameter [note]
- **Commands/OnboardCommand.cs** (Line 93): Redundant explicit collection creation in argument of 'params' parameter [note]
- **Commands/SkillCommand.cs** (Line 81): Redundant explicit collection creation in argument of 'params' parameter [note]
## RedundantNameQualifier
Found 35 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 25): Qualifier is redundant [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 35): Qualifier is redundant [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 60): Qualifier is redundant [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 64): Qualifier is redundant [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 92): Qualifier is redundant [warning]
- **Api/OpenAiCompatibleClient.cs** (Line 96): Qualifier is redundant [warning]
- **Commands/ConfigUpdaterCommand.cs** (Line 11): Qualifier is redundant [warning]
- **Commands/OnboardCommand.cs** (Line 15): Qualifier is redundant [warning]
- **Commands/OnboardCommand.cs** (Line 124): Qualifier is redundant [warning]
- **Commands/ShowCommand.cs** (Line 11): Qualifier is redundant [warning]
- **Configuration/ToakConfig.cs** (Line 15): Qualifier is redundant [warning]
- **Configuration/ToakConfig.cs** (Line 17): Qualifier is redundant [warning]
- **Core/Interfaces/Interfaces.cs** (Line 16): Qualifier is redundant [warning]
- **Core/Interfaces/Interfaces.cs** (Line 21): Qualifier is redundant [warning]
- **Core/Interfaces/Interfaces.cs** (Line 22): Qualifier is redundant [warning]
- **Core/Skills/SkillDefinition.cs** (Line 7): Qualifier is redundant [warning]
- **Core/TranscriptionOrchestrator.cs** (Line 99): Qualifier is redundant [warning]
- **IO/ClipboardManager.cs** (Line 28): Qualifier is redundant [warning]
- **IO/ClipboardManager.cs** (Line 38): Qualifier is redundant [warning]
- **IO/Notifications.cs** (Line 19): Qualifier is redundant [warning]
- ... and 15 more occurrences.
## RedundantStringInterpolation
Found 6 occurrences.
- **IO/TextInjector.cs** (Line 28): Redundant string interpolation [note]
- **IO/TextInjector.cs** (Line 39): Redundant string interpolation [note]
- **IO/TextInjector.cs** (Line 50): Redundant string interpolation [note]
- **IO/TextInjector.cs** (Line 78): Redundant string interpolation [note]
- **IO/TextInjector.cs** (Line 90): Redundant string interpolation [note]
- **IO/TextInjector.cs** (Line 109): Redundant string interpolation [note]
## RedundantTypeDeclarationBody
Found 2 occurrences.
- **Serialization/AppJsonSerializerContext.cs** (Line 24): Redundant empty class declaration body [note]
- **Serialization/AppJsonSerializerContext.cs** (Line 30): Redundant empty class declaration body [note]
## RedundantUsingDirective
Found 62 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning]
- **Audio/AudioRecorder.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning]
- **Audio/FfmpegAudioRecorder.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning]
- **Audio/FfmpegAudioRecorder.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning]
- **Audio/FfmpegAudioRecorder.cs** (Line 6): Using directive is not required by the code and can be safely removed [warning]
- **Commands/ConfigUpdaterCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning]
- **Commands/DiscardCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning]
- **Commands/DiscardCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning]
- **Commands/HistoryCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning]
- **Commands/HistoryCommand.cs** (Line 2): Using directive is not required by the code and can be safely removed [warning]
- **Commands/HistoryCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning]
- **Commands/HistoryCommand.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning]
- **Commands/HistoryCommand.cs** (Line 5): Using directive is not required by the code and can be safely removed [warning]
- **Commands/HistoryCommand.cs** (Line 6): Using directive is not required by the code and can be safely removed [warning]
- **Commands/LatencyTestCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning]
- **Commands/LatencyTestCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning]
- **Commands/LatencyTestCommand.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning]
- **Commands/OnboardCommand.cs** (Line 1): Using directive is not required by the code and can be safely removed [warning]
- **Commands/OnboardCommand.cs** (Line 3): Using directive is not required by the code and can be safely removed [warning]
- **Commands/OnboardCommand.cs** (Line 4): Using directive is not required by the code and can be safely removed [warning]
- ... and 42 more occurrences.
## UnusedMember.Global
Found 2 occurrences.
- **Core/Interfaces/Interfaces.cs** (Line 41): Method 'InjectTextAsync' is never used [note]
- **Core/Skills/ISkill.cs** (Line 6): Property 'Description' is never used [note]
## UnusedMemberInSuper.Global
Found 3 occurrences.
- **Core/Interfaces/Interfaces.cs** (Line 48): Only implementations of method 'ClearHistory' are used [note]
- **Core/Interfaces/Interfaces.cs** (Line 47): Only implementations of method 'LoadHistory' are used [note]
- **Core/Interfaces/Interfaces.cs** (Line 11): Only implementations of method 'SaveConfig' are used [note]
## UnusedParameter.Global
Found 1 occurrences.
- **Commands/SkillCommand.cs** (Line 14): Parameter 'verboseOption' is never used [note]
## UnusedVariable
Found 2 occurrences.
- **Commands/LatencyTestCommand.cs** (Line 60): Local variable 'refinedText' is never used [warning]
- **Commands/LatencyTestCommand.cs** (Line 54): Local variable 'transcript' is never used [warning]
## UseAwaitUsing
Found 3 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 29): Use 'await using' [note]
- **Api/OpenAiCompatibleClient.cs** (Line 122): Use 'await using' [note]
- **Commands/HistoryCommand.cs** (Line 57): Use 'await using' [note]
## UseCollectionExpression
Found 12 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 67): Use collection expression [note]
- **Api/OpenAiCompatibleClient.cs** (Line 100): Use collection expression [note]
- **Commands/StatsCommand.cs** (Line 33): Use collection expression [note]
- **Configuration/ToakConfig.cs** (Line 20): Use collection expression [note]
- **Core/HistoryManager.cs** (Line 43): Use collection expression [note]
- **Core/Skills/SkillRegistry.cs** (Line 73): Use collection expression [note]
- **Core/Skills/SkillRegistry.cs** (Line 83): Use collection expression [note]
- **Core/Skills/SkillRegistry.cs** (Line 94): Use collection expression [note]
- **Core/Skills/SkillRegistry.cs** (Line 108): Use collection expression [note]
- **Program.cs** (Line 13): Use collection expression [note]
- **Program.cs** (Line 15): Use collection expression [note]
- **Program.cs** (Line 87): Use collection expression [note]
## UsingStatementResourceInitialization
Found 1 occurrences.
- **Api/OpenAiCompatibleClient.cs** (Line 109): Initialize object properties inside the 'using' statement to ensure that the object is disposed if an exception is thrown during initialization [warning]