refactor: modernize code, improve performance, and clean up various components.
This commit is contained in:
@@ -15,7 +15,7 @@ public class OpenAiRequest
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = "llama-3.1-8b-instant";
|
||||
[JsonPropertyName("messages")]
|
||||
public OpenAiRequestMessage[] Messages { get; set; } = Array.Empty<OpenAiRequestMessage>();
|
||||
public OpenAiRequestMessage[] Messages { get; set; } = [];
|
||||
[JsonPropertyName("temperature")]
|
||||
public double Temperature { get; set; } = 0.0;
|
||||
[JsonPropertyName("stream")]
|
||||
@@ -27,7 +27,7 @@ public class OpenAiRequest
|
||||
public class OpenAiResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public OpenAiChoice[] Choices { get; set; } = Array.Empty<OpenAiChoice>();
|
||||
public OpenAiChoice[] Choices { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OpenAiChoice
|
||||
@@ -39,7 +39,7 @@ public class OpenAiChoice
|
||||
public class OpenAiStreamResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public OpenAiStreamChoice[] Choices { get; set; } = Array.Empty<OpenAiStreamChoice>();
|
||||
public OpenAiStreamChoice[] Choices { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OpenAiStreamChoice
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Toak.Api.Models;
|
||||
using Toak.Serialization;
|
||||
using Toak.Core;
|
||||
@@ -22,17 +20,17 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient
|
||||
_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)
|
||||
using var content = new MultipartFormDataContent();
|
||||
using var fileStream = File.OpenRead(filePath);
|
||||
await using var fileStream = File.OpenRead(filePath);
|
||||
using var streamContent = new StreamContent(fileStream);
|
||||
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); // or mpeg
|
||||
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");
|
||||
|
||||
@@ -57,18 +55,18 @@ public class OpenAiCompatibleClient : ISpeechClient, ILlmClient
|
||||
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
|
||||
{
|
||||
Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model,
|
||||
Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model,
|
||||
Temperature = 0.0,
|
||||
ReasoningEffort = _reasoningEffort,
|
||||
Messages = new[]
|
||||
{
|
||||
Messages =
|
||||
[
|
||||
new OpenAiRequestMessage { Role = "system", Content = systemPrompt },
|
||||
new OpenAiRequestMessage { Role = "user", Content = $"<transcript>{rawTranscript}</transcript>" }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Model = string.IsNullOrWhiteSpace(model) ? Toak.Core.Constants.Defaults.LlmModel : model,
|
||||
Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model,
|
||||
Temperature = 0.0,
|
||||
Stream = true,
|
||||
ReasoningEffort = _reasoningEffort,
|
||||
Messages = new[]
|
||||
{
|
||||
Messages =
|
||||
[
|
||||
new OpenAiRequestMessage { Role = "system", Content = systemPrompt },
|
||||
new OpenAiRequestMessage { Role = "user", Content = $"<transcript>{rawTranscript}</transcript>" }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Toak.Core;
|
||||
using Toak.IO;
|
||||
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.Audio;
|
||||
|
||||
public class AudioRecorder : IAudioRecorder
|
||||
public class AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) : IAudioRecorder
|
||||
{
|
||||
private readonly string WavPath = Constants.Paths.RecordingWavFile;
|
||||
private readonly IRecordingStateTracker _stateTracker;
|
||||
private readonly INotifications _notifications;
|
||||
private readonly string _wavPath = Constants.Paths.RecordingWavFile;
|
||||
private readonly IRecordingStateTracker _stateTracker = stateTracker;
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications)
|
||||
{
|
||||
_stateTracker = stateTracker;
|
||||
_notifications = notifications;
|
||||
}
|
||||
|
||||
public string GetWavPath() => WavPath;
|
||||
public string GetWavPath() => _wavPath;
|
||||
|
||||
public void StartRecording()
|
||||
{
|
||||
if (File.Exists(WavPath))
|
||||
if (File.Exists(_wavPath))
|
||||
{
|
||||
Logger.LogDebug($"Deleting old audio file: {WavPath}");
|
||||
File.Delete(WavPath);
|
||||
Logger.LogDebug($"Deleting old audio file: {_wavPath}");
|
||||
File.Delete(_wavPath);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Starting pw-record to record audio...");
|
||||
@@ -34,7 +26,7 @@ public class AudioRecorder : IAudioRecorder
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.AudioRecord,
|
||||
Arguments = $"--rate=16000 --channels=1 --format=s16 \"{WavPath}\"",
|
||||
Arguments = $"--rate=16000 --channels=1 --format=s16 \"{_wavPath}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
using Toak.Core;
|
||||
using Toak.IO;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.Audio;
|
||||
|
||||
public class FfmpegAudioRecorder : IAudioRecorder
|
||||
public class FfmpegAudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications) : IAudioRecorder
|
||||
{
|
||||
private readonly string WavPath = Constants.Paths.RecordingWavFile;
|
||||
private readonly IRecordingStateTracker _stateTracker;
|
||||
private readonly INotifications _notifications;
|
||||
private readonly string _wavPath = Constants.Paths.RecordingWavFile;
|
||||
private readonly IRecordingStateTracker _stateTracker = stateTracker;
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public FfmpegAudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications)
|
||||
{
|
||||
_stateTracker = stateTracker;
|
||||
_notifications = notifications;
|
||||
}
|
||||
|
||||
public string GetWavPath() => WavPath;
|
||||
public string GetWavPath() => _wavPath;
|
||||
|
||||
public void StartRecording()
|
||||
{
|
||||
if (File.Exists(WavPath))
|
||||
if (File.Exists(_wavPath))
|
||||
{
|
||||
Logger.LogDebug($"Deleting old audio file: {WavPath}");
|
||||
File.Delete(WavPath);
|
||||
Logger.LogDebug($"Deleting old audio file: {_wavPath}");
|
||||
File.Delete(_wavPath);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Starting ffmpeg to record audio...");
|
||||
@@ -35,7 +25,7 @@ public class FfmpegAudioRecorder : IAudioRecorder
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
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,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
@@ -53,35 +43,31 @@ public class FfmpegAudioRecorder : IAudioRecorder
|
||||
public void StopRecording()
|
||||
{
|
||||
var pid = _stateTracker.GetRecordingPid();
|
||||
if (pid.HasValue)
|
||||
if (!pid.HasValue) return;
|
||||
Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop...");
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop...");
|
||||
try
|
||||
var process = Process.GetProcessById(pid.Value);
|
||||
if (process.HasExited) return;
|
||||
// Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
var process = Process.GetProcessById(pid.Value);
|
||||
if (!process.HasExited)
|
||||
{
|
||||
// Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.ProcessKill,
|
||||
Arguments = $"-INT {pid.Value}",
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false
|
||||
})?.WaitForExit();
|
||||
FileName = Constants.Commands.ProcessKill,
|
||||
Arguments = $"-INT {pid.Value}",
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false
|
||||
})?.WaitForExit();
|
||||
|
||||
process.WaitForExit(2000); // give it a moment to flush
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Process might already be dead
|
||||
Console.WriteLine($"[FfmpegAudioRecorder] Error stopping ffmpeg: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateTracker.ClearRecording();
|
||||
}
|
||||
process.WaitForExit(2000); // give it a moment to flush
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Process might already be dead
|
||||
Console.WriteLine($"[FfmpegAudioRecorder] Error stopping ffmpeg: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateTracker.ClearRecording();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Configuration;
|
||||
|
||||
@@ -6,9 +5,9 @@ namespace Toak.Commands;
|
||||
|
||||
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 config = configManager.LoadConfig();
|
||||
key = key.ToLowerInvariant();
|
||||
@@ -23,18 +22,19 @@ public static class ConfigUpdaterCommand
|
||||
case "backend": config.TypingBackend = val; break;
|
||||
case "punctuation":
|
||||
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;
|
||||
case "tech":
|
||||
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;
|
||||
default:
|
||||
AnsiConsole.MarkupLine($"[red]Unknown config key: {key}[/]");
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
configManager.SaveConfig(config);
|
||||
AnsiConsole.MarkupLine($"[green]Successfully[/] set {key} to [blue]{val}[/].");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core;
|
||||
|
||||
|
||||
@@ -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 Toak.Core;
|
||||
|
||||
@@ -54,15 +48,15 @@ public static class HistoryCommand
|
||||
{
|
||||
try
|
||||
{
|
||||
using var writer = new StreamWriter(export);
|
||||
writer.WriteLine($"# Toak Transcriptions - {DateTime.Now:yyyy-MM-dd}");
|
||||
writer.WriteLine();
|
||||
await using var writer = new StreamWriter(export);
|
||||
await writer.WriteLineAsync($"# Toak Transcriptions - {DateTime.Now:yyyy-MM-dd}");
|
||||
await writer.WriteLineAsync();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
writer.WriteLine($"## {entry.Timestamp.ToLocalTime():HH:mm:ss}");
|
||||
writer.WriteLine(entry.RefinedText);
|
||||
writer.WriteLine();
|
||||
await writer.WriteLineAsync($"## {entry.Timestamp.ToLocalTime():HH:mm:ss}");
|
||||
await writer.WriteLineAsync(entry.RefinedText);
|
||||
await writer.WriteLineAsync();
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[green]Successfully exported {entries.Count} entries to {export}[/]");
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Api;
|
||||
using Toak.Configuration;
|
||||
@@ -34,7 +31,7 @@ public static class LatencyTestCommand
|
||||
RedirectStandardOutput = true
|
||||
};
|
||||
var proc = Process.Start(pInfo);
|
||||
proc?.WaitForExit();
|
||||
if (proc != null) await proc.WaitForExitAsync();
|
||||
|
||||
if (!File.Exists(testWavPath))
|
||||
{
|
||||
@@ -51,13 +48,13 @@ public static class LatencyTestCommand
|
||||
{
|
||||
ctx.Status("Testing STT (Whisper)...");
|
||||
var sttWatch = Stopwatch.StartNew();
|
||||
var transcript = await client.TranscribeAsync(testWavPath, config.WhisperLanguage, config.WhisperModel);
|
||||
await client.TranscribeAsync(testWavPath, config.WhisperLanguage, config.WhisperModel);
|
||||
sttWatch.Stop();
|
||||
|
||||
ctx.Status("Testing LLM (Llama)...");
|
||||
var systemPrompt = PromptBuilder.BuildPrompt(config);
|
||||
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();
|
||||
|
||||
var total = sttWatch.ElapsedMilliseconds + llmWatch.ElapsedMilliseconds;
|
||||
@@ -73,14 +70,9 @@ public static class LatencyTestCommand
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
|
||||
if (total < 1500)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[green]Status: OK (under 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.[/]");
|
||||
}
|
||||
AnsiConsole.MarkupLine(total < 1500
|
||||
? $"[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.[/]");
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Configuration;
|
||||
using Toak.Core.Skills;
|
||||
@@ -12,7 +9,7 @@ public static class OnboardCommand
|
||||
{
|
||||
public static async Task ExecuteAsync(bool verbose)
|
||||
{
|
||||
Toak.Core.Logger.Verbose = verbose;
|
||||
Core.Logger.Verbose = verbose;
|
||||
var configManager = new ConfigManager();
|
||||
var config = configManager.LoadConfig();
|
||||
|
||||
@@ -121,7 +118,7 @@ public static class OnboardCommand
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Toak.Core.Logger.LogDebug($"Failed to restart toak service: {ex.Message}");
|
||||
Core.Logger.LogDebug($"Failed to restart toak service: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Configuration;
|
||||
|
||||
@@ -8,7 +7,7 @@ public static class ShowCommand
|
||||
{
|
||||
public static async Task ExecuteAsync(bool verbose)
|
||||
{
|
||||
Toak.Core.Logger.Verbose = verbose;
|
||||
Core.Logger.Verbose = verbose;
|
||||
var config = new ConfigManager().LoadConfig();
|
||||
|
||||
var table = new Table();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core.Skills;
|
||||
using Toak.Serialization;
|
||||
@@ -100,8 +97,8 @@ public static class SkillCommand
|
||||
};
|
||||
|
||||
SkillRegistry.Initialize(); // ensure dir exists
|
||||
string filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json");
|
||||
string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
|
||||
var filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json");
|
||||
var json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
|
||||
File.WriteAllText(filename, json);
|
||||
|
||||
AnsiConsole.MarkupLine($"[bold green]Success![/] Skill '{name}' saved to {filename}");
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core;
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.CommandLine;
|
||||
using Spectre.Console;
|
||||
using Toak.Core;
|
||||
|
||||
@@ -9,7 +5,7 @@ namespace Toak.Commands;
|
||||
|
||||
public static class StatsCommand
|
||||
{
|
||||
public static async Task ExecuteAsync(bool verbose)
|
||||
public static Task ExecuteAsync(bool verbose)
|
||||
{
|
||||
Logger.Verbose = verbose;
|
||||
|
||||
@@ -17,7 +13,7 @@ public static class StatsCommand
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]No history found. Cannot generate statistics.[/]");
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var totalCount = entries.Count;
|
||||
@@ -30,7 +26,7 @@ public static class StatsCommand
|
||||
.FirstOrDefault();
|
||||
|
||||
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
|
||||
.GroupBy(w => w.ToLowerInvariant())
|
||||
.OrderByDescending(g => g.Count())
|
||||
@@ -52,5 +48,6 @@ public static class StatsCommand
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[dim]Top spoken words (>3 chars):[/] {string.Join(", ", topWords)}");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core;
|
||||
|
||||
@@ -24,7 +22,7 @@ public static class StatusCommand
|
||||
await socket.SendAsync(msg, SocketFlags.None);
|
||||
|
||||
var responseBuffer = new byte[4096];
|
||||
int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
|
||||
var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
|
||||
if (received > 0)
|
||||
{
|
||||
var text = System.Text.Encoding.UTF8.GetString(responseBuffer, 0, received);
|
||||
@@ -33,10 +31,7 @@ public static class StatusCommand
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
if (json)
|
||||
Console.WriteLine("{\"state\": \"Offline\"}");
|
||||
else
|
||||
Console.WriteLine("Offline");
|
||||
Console.WriteLine(json ? "{\"state\": \"Offline\"}" : "Offline");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core;
|
||||
|
||||
@@ -25,7 +23,7 @@ public static class StopCommand
|
||||
var responseBuffer = new byte[4096];
|
||||
while (true)
|
||||
{
|
||||
int received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
|
||||
var received = await socket.ReceiveAsync(responseBuffer, SocketFlags.None);
|
||||
if (received == 0) break;
|
||||
if (pipeToStdout)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core;
|
||||
|
||||
@@ -34,7 +32,7 @@ public static class ToggleCommand
|
||||
var responseBuffer = new byte[4096];
|
||||
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 (pipeToStdout)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Toak.Core;
|
||||
using Toak.Serialization;
|
||||
using Toak.Core.Interfaces;
|
||||
@@ -9,23 +7,19 @@ namespace Toak.Configuration;
|
||||
|
||||
public class ConfigManager : IConfigProvider
|
||||
{
|
||||
private readonly string ConfigDir = Constants.Paths.ConfigDir;
|
||||
private readonly string ConfigPath = Constants.Paths.ConfigFile;
|
||||
|
||||
public ConfigManager()
|
||||
{
|
||||
}
|
||||
private readonly string _configDir = Constants.Paths.ConfigDir;
|
||||
private readonly string _configPath = Constants.Paths.ConfigFile;
|
||||
|
||||
public ToakConfig LoadConfig()
|
||||
{
|
||||
if (!File.Exists(ConfigPath))
|
||||
if (!File.Exists(_configPath))
|
||||
{
|
||||
return new ToakConfig();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(ConfigPath);
|
||||
var json = File.ReadAllText(_configPath);
|
||||
return JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.ToakConfig) ?? new ToakConfig();
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -36,12 +30,12 @@ public class ConfigManager : IConfigProvider
|
||||
|
||||
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);
|
||||
File.WriteAllText(ConfigPath, json);
|
||||
File.WriteAllText(_configPath, json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ public class ToakConfig
|
||||
public int MinRecordingDuration { get; set; } = 500;
|
||||
|
||||
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 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 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"];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Toak.Core;
|
||||
|
||||
public static class Constants
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Toak.Configuration;
|
||||
using Toak.Api;
|
||||
using Toak.Core.Interfaces;
|
||||
@@ -54,15 +50,9 @@ public static class DaemonService
|
||||
var notifications = new Notifications();
|
||||
|
||||
var speechClient = new OpenAiCompatibleClient(config.GroqApiKey);
|
||||
ILlmClient llmClient;
|
||||
if (config.LlmProvider == "together")
|
||||
{
|
||||
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);
|
||||
}
|
||||
ILlmClient llmClient = 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);
|
||||
|
||||
IAudioRecorder recorder = config.AudioBackend == "ffmpeg"
|
||||
? new FfmpegAudioRecorder(stateTracker, notifications)
|
||||
@@ -114,12 +104,12 @@ public static class DaemonService
|
||||
try
|
||||
{
|
||||
var buffer = new byte[3];
|
||||
int bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None);
|
||||
var bytesRead = await client.ReceiveAsync(buffer, SocketFlags.None);
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
byte cmd = buffer[0];
|
||||
bool pipeToStdout = bytesRead > 1 && buffer[1] == 1;
|
||||
bool copyToClipboard = bytesRead > 2 && buffer[2] == 1;
|
||||
var cmd = buffer[0];
|
||||
var pipeToStdout = bytesRead > 1 && buffer[1] == 1;
|
||||
var copyToClipboard = bytesRead > 2 && buffer[2] == 1;
|
||||
|
||||
if (cmd == 1) // START
|
||||
{
|
||||
@@ -142,9 +132,9 @@ public static class DaemonService
|
||||
}
|
||||
else if (cmd == 5) // STATUS
|
||||
{
|
||||
bool json = pipeToStdout; // buffer[1] == 1 is json
|
||||
bool isRecording = stateTracker.IsRecording();
|
||||
string stateStr = isRecording ? "Recording" : "Idle";
|
||||
var json = pipeToStdout; // buffer[1] == 1 is json
|
||||
var isRecording = stateTracker.IsRecording();
|
||||
var stateStr = isRecording ? "Recording" : "Idle";
|
||||
|
||||
if (json)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
|
||||
namespace Toak.Core;
|
||||
|
||||
public class HistoryEntry
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Toak.Serialization;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
@@ -10,20 +6,16 @@ namespace Toak.Core;
|
||||
|
||||
public class HistoryManager : IHistoryManager
|
||||
{
|
||||
private readonly string HistoryDir = Constants.Paths.AppDataDir;
|
||||
private readonly string HistoryFile = Constants.Paths.HistoryFile;
|
||||
|
||||
public HistoryManager()
|
||||
{
|
||||
}
|
||||
private readonly string _historyDir = Constants.Paths.AppDataDir;
|
||||
private readonly string _historyFile = Constants.Paths.HistoryFile;
|
||||
|
||||
public void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(HistoryDir))
|
||||
if (!Directory.Exists(_historyDir))
|
||||
{
|
||||
Directory.CreateDirectory(HistoryDir);
|
||||
Directory.CreateDirectory(_historyDir);
|
||||
}
|
||||
|
||||
var entry = new HistoryEntry
|
||||
@@ -38,9 +30,9 @@ public class HistoryManager : IHistoryManager
|
||||
var json = JsonSerializer.Serialize(entry, CompactJsonSerializerContext.Default.HistoryEntry);
|
||||
|
||||
// Thread-safe append
|
||||
lock (HistoryFile)
|
||||
lock (_historyFile)
|
||||
{
|
||||
File.AppendAllLines(HistoryFile, new[] { json });
|
||||
File.AppendAllLines(_historyFile, [json]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -52,14 +44,14 @@ public class HistoryManager : IHistoryManager
|
||||
public List<HistoryEntry> LoadHistory()
|
||||
{
|
||||
var entries = new List<HistoryEntry>();
|
||||
if (!File.Exists(HistoryFile)) return entries;
|
||||
if (!File.Exists(_historyFile)) return entries;
|
||||
|
||||
try
|
||||
{
|
||||
string[] lines;
|
||||
lock (HistoryFile)
|
||||
lock (_historyFile)
|
||||
{
|
||||
lines = File.ReadAllLines(HistoryFile);
|
||||
lines = File.ReadAllLines(_historyFile);
|
||||
}
|
||||
|
||||
foreach (var line in lines)
|
||||
@@ -92,20 +84,20 @@ public class HistoryManager : IHistoryManager
|
||||
|
||||
public void ClearHistory()
|
||||
{
|
||||
if (File.Exists(HistoryFile))
|
||||
if (File.Exists(_historyFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (HistoryFile)
|
||||
lock (_historyFile)
|
||||
{
|
||||
// Securely delete
|
||||
var len = new FileInfo(HistoryFile).Length;
|
||||
using (var fs = new FileStream(HistoryFile, FileMode.Open, FileAccess.Write))
|
||||
var len = new FileInfo(_historyFile).Length;
|
||||
using (var fs = new FileStream(_historyFile, FileMode.Open, FileAccess.Write))
|
||||
{
|
||||
var blank = new byte[len];
|
||||
fs.Write(blank, 0, blank.Length);
|
||||
}
|
||||
File.Delete(HistoryFile);
|
||||
File.Delete(_historyFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Toak.Core.Interfaces;
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Toak.Configuration;
|
||||
|
||||
namespace Toak.Core.Interfaces;
|
||||
@@ -13,13 +10,13 @@ public interface IConfigProvider
|
||||
|
||||
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
|
||||
{
|
||||
Task<string> RefineTextAsync(string rawTranscript, string systemPrompt, string model = Toak.Core.Constants.Defaults.LlmModel);
|
||||
IAsyncEnumerable<string> RefineTextStreamAsync(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 = Constants.Defaults.LlmModel);
|
||||
}
|
||||
|
||||
public interface IAudioRecorder
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text;
|
||||
|
||||
using Toak.Configuration;
|
||||
|
||||
namespace Toak.Core;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
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 Description => _def.Description;
|
||||
@@ -13,11 +12,6 @@ public class DynamicSkill : ISkill
|
||||
|
||||
public bool HandlesExecution => _def.Action.ToLowerInvariant() == "script";
|
||||
|
||||
public DynamicSkill(SkillDefinition def)
|
||||
{
|
||||
_def = def;
|
||||
}
|
||||
|
||||
public string GetSystemPrompt(string rawTranscript)
|
||||
{
|
||||
return _def.SystemPrompt.Replace("{transcript}", rawTranscript);
|
||||
|
||||
@@ -4,7 +4,7 @@ public class SkillDefinition
|
||||
{
|
||||
public string Name { 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 SystemPrompt { get; set; } = "";
|
||||
public string? ScriptPath { get; set; }
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Toak.Serialization;
|
||||
|
||||
@@ -9,7 +5,7 @@ namespace Toak.Core.Skills;
|
||||
|
||||
public static class SkillRegistry
|
||||
{
|
||||
public static List<ISkill> AllSkills = new List<ISkill>();
|
||||
public static List<ISkill> AllSkills = [];
|
||||
|
||||
public static string SkillsDirectory => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
@@ -28,7 +24,7 @@ public static class SkillRegistry
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
var json = File.ReadAllText(file);
|
||||
var def = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillDefinition);
|
||||
if (def != null)
|
||||
{
|
||||
@@ -47,7 +43,7 @@ public static class SkillRegistry
|
||||
if (AllSkills.Count == 0) Initialize();
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -72,9 +68,11 @@ public static class SkillRegistry
|
||||
Description = "Translates the spoken command into a bash command and types it.",
|
||||
Hotwords = ["System terminal", "System run", "System execute"],
|
||||
Action = "type",
|
||||
SystemPrompt = @"You are a Linux terminal expert.
|
||||
Translate the user's request into a single, valid bash command.
|
||||
Output ONLY the raw command, no formatting, no markdown."
|
||||
SystemPrompt = """
|
||||
You are a Linux terminal expert.
|
||||
Translate the user's request into a single, valid bash command.
|
||||
Output ONLY the raw command, no formatting, no markdown.
|
||||
"""
|
||||
},
|
||||
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.",
|
||||
Hotwords = ["System translate to", "System translate into"],
|
||||
Action = "type",
|
||||
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:').
|
||||
Translate the REST of the transcript into that target language.
|
||||
Output ONLY the final translated text. Do not include markdown, explanations, or quotes."
|
||||
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:').
|
||||
Translate the REST of the transcript into that target language.
|
||||
Output ONLY the final translated text. Do not include markdown, explanations, or quotes.
|
||||
"""
|
||||
},
|
||||
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.",
|
||||
Hotwords = ["System professional", "System formalize", "System formal"],
|
||||
Action = "type",
|
||||
SystemPrompt = @"Rewrite the following text to be articulate and 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.
|
||||
Do not add any conversational filler.
|
||||
Make sure to preserve the meaning of the original text.
|
||||
Output ONLY the final professional text.
|
||||
Text: {transcript}"
|
||||
SystemPrompt = """
|
||||
Rewrite the following text to be articulate and 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.
|
||||
Do not add any conversational filler.
|
||||
Make sure to preserve the meaning of the original text.
|
||||
Output ONLY the final professional text.
|
||||
Text: {transcript}
|
||||
"""
|
||||
},
|
||||
new SkillDefinition
|
||||
{
|
||||
@@ -107,19 +109,21 @@ Text: {transcript}"
|
||||
Description = "Provides a direct, crisp summary of the dictation.",
|
||||
Hotwords = ["System summary", "System concise", "System summarize"],
|
||||
Action = "type",
|
||||
SystemPrompt = @"Summarize the following text to be as concise
|
||||
and direct as possible.
|
||||
The text will start with 'System summary', 'System concise', or 'System summarize',
|
||||
and you shoul ignore that part of the text.
|
||||
Output ONLY the final summary text.
|
||||
Text: {transcript}"
|
||||
SystemPrompt = """
|
||||
Summarize the following text to be as concise
|
||||
and direct as possible.
|
||||
The text will start with 'System summary', 'System concise', or 'System summarize',
|
||||
and you shoul ignore that part of the text.
|
||||
Output ONLY the final summary text.
|
||||
Text: {transcript}
|
||||
"""
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var def in defaults)
|
||||
{
|
||||
string filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json");
|
||||
string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
|
||||
var filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json");
|
||||
var json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
|
||||
File.WriteAllText(filename, json);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,24 @@ namespace Toak.Core;
|
||||
|
||||
public class StateTracker : IRecordingStateTracker
|
||||
{
|
||||
private readonly string StateFilePath = Constants.Paths.StateFile;
|
||||
private readonly string _stateFilePath = Constants.Paths.StateFile;
|
||||
|
||||
public bool IsRecording()
|
||||
{
|
||||
return File.Exists(StateFilePath);
|
||||
return File.Exists(_stateFilePath);
|
||||
}
|
||||
|
||||
public void SetRecording(int 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()
|
||||
{
|
||||
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))
|
||||
{
|
||||
Logger.LogDebug($"Read recording PID {pid} from state file");
|
||||
@@ -33,9 +33,9 @@ public class StateTracker : IRecordingStateTracker
|
||||
|
||||
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))
|
||||
{
|
||||
return new DateTime(ticks, DateTimeKind.Utc);
|
||||
@@ -46,10 +46,10 @@ public class StateTracker : IRecordingStateTracker
|
||||
|
||||
public void ClearRecording()
|
||||
{
|
||||
if (File.Exists(StateFilePath))
|
||||
if (File.Exists(_stateFilePath))
|
||||
{
|
||||
Logger.LogDebug("Clearing recording state file");
|
||||
File.Delete(StateFilePath);
|
||||
File.Delete(_stateFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,39 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Toak.Core.Interfaces;
|
||||
using Toak.Configuration;
|
||||
|
||||
namespace Toak.Core;
|
||||
|
||||
public class TranscriptionOrchestrator : ITranscriptionOrchestrator
|
||||
public class TranscriptionOrchestrator(
|
||||
ISpeechClient speechClient,
|
||||
ILlmClient llmClient,
|
||||
IConfigProvider configProvider,
|
||||
IAudioRecorder audioRecorder,
|
||||
INotifications notifications,
|
||||
ITextInjector textInjector,
|
||||
IHistoryManager historyManager,
|
||||
IClipboardManager clipboardManager,
|
||||
IRecordingStateTracker stateTracker) : ITranscriptionOrchestrator
|
||||
{
|
||||
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;
|
||||
private readonly ISpeechClient _speechClient = speechClient;
|
||||
private readonly ILlmClient _llmClient = llmClient;
|
||||
private readonly IConfigProvider _configProvider = configProvider;
|
||||
private readonly IAudioRecorder _audioRecorder = audioRecorder;
|
||||
private readonly INotifications _notifications = notifications;
|
||||
private readonly ITextInjector _textInjector = textInjector;
|
||||
private readonly IHistoryManager _historyManager = historyManager;
|
||||
private readonly IClipboardManager _clipboardManager = clipboardManager;
|
||||
private readonly IRecordingStateTracker _stateTracker = stateTracker;
|
||||
|
||||
public TranscriptionOrchestrator(
|
||||
ISpeechClient speechClient,
|
||||
ILlmClient llmClient,
|
||||
IConfigProvider configProvider,
|
||||
IAudioRecorder audioRecorder,
|
||||
INotifications notifications,
|
||||
ITextInjector textInjector,
|
||||
IHistoryManager historyManager,
|
||||
IClipboardManager clipboardManager,
|
||||
IRecordingStateTracker stateTracker)
|
||||
public Task ProcessStartRecordingAsync()
|
||||
{
|
||||
_speechClient = speechClient;
|
||||
_llmClient = llmClient;
|
||||
_configProvider = configProvider;
|
||||
_audioRecorder = audioRecorder;
|
||||
_notifications = notifications;
|
||||
_textInjector = textInjector;
|
||||
_historyManager = historyManager;
|
||||
_clipboardManager = clipboardManager;
|
||||
_stateTracker = stateTracker;
|
||||
}
|
||||
|
||||
public async Task ProcessStartRecordingAsync()
|
||||
{
|
||||
if (_stateTracker.IsRecording()) return;
|
||||
if (_stateTracker.IsRecording()) return Task.CompletedTask;
|
||||
|
||||
Logger.LogDebug("Received START command");
|
||||
var config = _configProvider.LoadConfig();
|
||||
_notifications.PlaySound(config.StartSoundPath);
|
||||
_audioRecorder.StartRecording();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard)
|
||||
@@ -96,9 +80,9 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
|
||||
return;
|
||||
}
|
||||
|
||||
var detectedSkill = Toak.Core.Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills);
|
||||
string systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config);
|
||||
bool isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution;
|
||||
var detectedSkill = Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills);
|
||||
var systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config);
|
||||
var isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution;
|
||||
|
||||
if (isExecutionSkill)
|
||||
{
|
||||
@@ -118,7 +102,7 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
|
||||
|
||||
if (pipeToStdout || copyToClipboard)
|
||||
{
|
||||
string fullText = "";
|
||||
var fullText = "";
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
fullText += token;
|
||||
@@ -137,7 +121,7 @@ public class TranscriptionOrchestrator : ITranscriptionOrchestrator
|
||||
}
|
||||
else
|
||||
{
|
||||
string fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend);
|
||||
var fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend);
|
||||
stopWatch.Stop();
|
||||
_historyManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds);
|
||||
_notifications.Notify("Toak", $"Done in {stopWatch.ElapsedMilliseconds}ms");
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO;
|
||||
|
||||
public class ClipboardManager : IClipboardManager
|
||||
public class ClipboardManager(INotifications notifications) : IClipboardManager
|
||||
{
|
||||
private readonly INotifications _notifications;
|
||||
|
||||
public ClipboardManager(INotifications notifications)
|
||||
{
|
||||
_notifications = notifications;
|
||||
}
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public void Copy(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
try
|
||||
{
|
||||
string sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLowerInvariant() ?? "";
|
||||
var sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLowerInvariant() ?? "";
|
||||
|
||||
ProcessStartInfo pInfo;
|
||||
if (sessionType == "wayland")
|
||||
{
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.ClipboardWayland,
|
||||
FileName = Core.Constants.Commands.ClipboardWayland,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
@@ -35,7 +29,7 @@ public class ClipboardManager : IClipboardManager
|
||||
{
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.ClipboardX11,
|
||||
FileName = Core.Constants.Commands.ClipboardX11,
|
||||
Arguments = "-selection clipboard",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
@@ -44,14 +38,12 @@ public class ClipboardManager : IClipboardManager
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
process.WaitForExit();
|
||||
sw.Write(text);
|
||||
}
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO;
|
||||
@@ -16,7 +15,7 @@ public class Notifications : INotifications
|
||||
{
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.Notify,
|
||||
FileName = Core.Constants.Commands.Notify,
|
||||
Arguments = $"-a \"Toak\" \"{summary}\" \"{body}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
@@ -66,7 +65,7 @@ public class Notifications : INotifications
|
||||
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.PlaySound,
|
||||
FileName = Core.Constants.Commands.PlaySound,
|
||||
Arguments = $"\"{absolutePath}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Toak.Core;
|
||||
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO;
|
||||
|
||||
public class TextInjector : ITextInjector
|
||||
public class TextInjector(INotifications notifications) : ITextInjector
|
||||
{
|
||||
private readonly INotifications _notifications;
|
||||
|
||||
public TextInjector(INotifications notifications)
|
||||
{
|
||||
_notifications = notifications;
|
||||
}
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public Task InjectTextAsync(string text, string backend = "xdotool")
|
||||
{
|
||||
@@ -28,8 +21,8 @@ public class TextInjector : ITextInjector
|
||||
Logger.LogDebug($"Injecting text using wtype...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
@@ -39,7 +32,7 @@ public class TextInjector : ITextInjector
|
||||
Logger.LogDebug($"Injecting text using ydotool...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.TypeYdotool,
|
||||
FileName = Constants.Commands.TypeYdotool,
|
||||
Arguments = $"type \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
@@ -50,8 +43,8 @@ public class TextInjector : ITextInjector
|
||||
Logger.LogDebug($"Injecting text using xdotool...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
FileName = Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
@@ -69,7 +62,7 @@ public class TextInjector : ITextInjector
|
||||
|
||||
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream, string backend)
|
||||
{
|
||||
string fullText = string.Empty;
|
||||
var fullText = string.Empty;
|
||||
try
|
||||
{
|
||||
ProcessStartInfo pInfo;
|
||||
@@ -78,8 +71,8 @@ public class TextInjector : ITextInjector
|
||||
Logger.LogDebug($"Setting up stream injection using wtype...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} -",
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} -",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
@@ -94,7 +87,7 @@ public class TextInjector : ITextInjector
|
||||
fullText += token;
|
||||
var chunkInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.TypeYdotool,
|
||||
FileName = Constants.Commands.TypeYdotool,
|
||||
Arguments = $"type \"{token.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
@@ -109,8 +102,8 @@ public class TextInjector : ITextInjector
|
||||
Logger.LogDebug($"Setting up stream injection using xdotool...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Toak.Core.Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Toak.Core.Constants.Defaults.DefaultTypeDelayMs} --file -",
|
||||
FileName = Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} --file -",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.CommandLine;
|
||||
using System.Threading.Tasks;
|
||||
using Toak.Commands;
|
||||
|
||||
namespace Toak;
|
||||
@@ -44,7 +43,7 @@ public class Program
|
||||
|
||||
// Daemon Command
|
||||
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);
|
||||
|
||||
// Discard Command
|
||||
|
||||
@@ -18,14 +18,14 @@ namespace Toak.Serialization;
|
||||
[JsonSerializable(typeof(OpenAiStreamChoice))]
|
||||
[JsonSerializable(typeof(OpenAiStreamDelta))]
|
||||
[JsonSerializable(typeof(OpenAiStreamChoice[]))]
|
||||
[JsonSerializable(typeof(Toak.Core.Skills.SkillDefinition))]
|
||||
[JsonSerializable(typeof(Toak.Core.HistoryEntry))]
|
||||
[JsonSerializable(typeof(Core.Skills.SkillDefinition))]
|
||||
[JsonSerializable(typeof(Core.HistoryEntry))]
|
||||
internal partial class AppJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(Toak.Core.HistoryEntry))]
|
||||
[JsonSerializable(typeof(Core.HistoryEntry))]
|
||||
internal partial class CompactJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
30
Toak.csproj
30
Toak.csproj
@@ -1,20 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\Audio\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\Audio\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user