refactor: modernize code, improve performance, and clean up various components.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,35 +43,31 @@ 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...");
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Logger.LogDebug($"Found active ffmpeg process with PID {pid.Value}. Attempting to stop...");
|
var process = Process.GetProcessById(pid.Value);
|
||||||
try
|
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);
|
FileName = Constants.Commands.ProcessKill,
|
||||||
if (!process.HasExited)
|
Arguments = $"-INT {pid.Value}",
|
||||||
{
|
CreateNoWindow = true,
|
||||||
// Gracefully stop ffmpeg using SIGINT to ensure WAV headers are finalizing cleanly
|
UseShellExecute = false
|
||||||
Process.Start(new ProcessStartInfo
|
})?.WaitForExit();
|
||||||
{
|
|
||||||
FileName = Constants.Commands.ProcessKill,
|
|
||||||
Arguments = $"-INT {pid.Value}",
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
})?.WaitForExit();
|
|
||||||
|
|
||||||
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
|
Console.WriteLine($"[FfmpegAudioRecorder] Error stopping ffmpeg: {ex.Message}");
|
||||||
Console.WriteLine($"[FfmpegAudioRecorder] Error stopping ffmpeg: {ex.Message}");
|
}
|
||||||
}
|
finally
|
||||||
finally
|
{
|
||||||
{
|
_stateTracker.ClearRecording();
|
||||||
_stateTracker.ClearRecording();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}[/]");
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Toak.Core;
|
namespace Toak.Core;
|
||||||
|
|
||||||
public static class Constants
|
public static class Constants
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Toak.Core;
|
namespace Toak.Core;
|
||||||
|
|
||||||
public class HistoryEntry
|
public class HistoryEntry
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Toak.Core.Interfaces;
|
namespace Toak.Core.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using Toak.Configuration;
|
using Toak.Configuration;
|
||||||
|
|
||||||
namespace Toak.Core;
|
namespace Toak.Core;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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 = """
|
||||||
Translate the user's request into a single, valid bash command.
|
You are a Linux terminal expert.
|
||||||
Output ONLY the raw command, no formatting, no markdown."
|
Translate the user's request into a single, valid bash command.
|
||||||
|
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 = """
|
||||||
The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:').
|
You are an expert translator. The user wants to translate the following text.
|
||||||
Translate the REST of the transcript into that target language.
|
The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:').
|
||||||
Output ONLY the final translated text. Do not include markdown, explanations, or quotes."
|
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
|
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 = """
|
||||||
The text will start with 'System professional', 'System formalize', or 'System formal',
|
Rewrite the following text to be articulate and formal.
|
||||||
or something along the lines of that. You can ignore those words.
|
The text will start with 'System professional', 'System formalize', or 'System formal',
|
||||||
Do not add any conversational filler.
|
or something along the lines of that. You can ignore those words.
|
||||||
Make sure to preserve the meaning of the original text.
|
Do not add any conversational filler.
|
||||||
Output ONLY the final professional text.
|
Make sure to preserve the meaning of the original text.
|
||||||
Text: {transcript}"
|
Output ONLY the final professional text.
|
||||||
|
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 = """
|
||||||
and direct as possible.
|
Summarize the following text to be as concise
|
||||||
The text will start with 'System summary', 'System concise', or 'System summarize',
|
and direct as possible.
|
||||||
and you shoul ignore that part of the text.
|
The text will start with 'System summary', 'System concise', or 'System summarize',
|
||||||
Output ONLY the final summary text.
|
and you shoul ignore that part of the text.
|
||||||
Text: {transcript}"
|
Output ONLY the final summary text.
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,39 @@
|
|||||||
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(
|
||||||
|
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 ISpeechClient _speechClient = speechClient;
|
||||||
private readonly ILlmClient _llmClient;
|
private readonly ILlmClient _llmClient = llmClient;
|
||||||
private readonly IConfigProvider _configProvider;
|
private readonly IConfigProvider _configProvider = configProvider;
|
||||||
private readonly IAudioRecorder _audioRecorder;
|
private readonly IAudioRecorder _audioRecorder = audioRecorder;
|
||||||
private readonly INotifications _notifications;
|
private readonly INotifications _notifications = notifications;
|
||||||
private readonly ITextInjector _textInjector;
|
private readonly ITextInjector _textInjector = textInjector;
|
||||||
private readonly IHistoryManager _historyManager;
|
private readonly IHistoryManager _historyManager = historyManager;
|
||||||
private readonly IClipboardManager _clipboardManager;
|
private readonly IClipboardManager _clipboardManager = clipboardManager;
|
||||||
private readonly IRecordingStateTracker _stateTracker;
|
private readonly IRecordingStateTracker _stateTracker = stateTracker;
|
||||||
|
|
||||||
public TranscriptionOrchestrator(
|
public Task ProcessStartRecordingAsync()
|
||||||
ISpeechClient speechClient,
|
|
||||||
ILlmClient llmClient,
|
|
||||||
IConfigProvider configProvider,
|
|
||||||
IAudioRecorder audioRecorder,
|
|
||||||
INotifications notifications,
|
|
||||||
ITextInjector textInjector,
|
|
||||||
IHistoryManager historyManager,
|
|
||||||
IClipboardManager clipboardManager,
|
|
||||||
IRecordingStateTracker stateTracker)
|
|
||||||
{
|
{
|
||||||
_speechClient = speechClient;
|
if (_stateTracker.IsRecording()) return Task.CompletedTask;
|
||||||
_llmClient = llmClient;
|
|
||||||
_configProvider = configProvider;
|
|
||||||
_audioRecorder = audioRecorder;
|
|
||||||
_notifications = notifications;
|
|
||||||
_textInjector = textInjector;
|
|
||||||
_historyManager = historyManager;
|
|
||||||
_clipboardManager = clipboardManager;
|
|
||||||
_stateTracker = stateTracker;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessStartRecordingAsync()
|
|
||||||
{
|
|
||||||
if (_stateTracker.IsRecording()) return;
|
|
||||||
|
|
||||||
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");
|
||||||
|
|||||||
@@ -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,14 +38,12 @@ 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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
28
Toak.csproj
28
Toak.csproj
@@ -1,20 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Assets\Audio\**" />
|
<EmbeddedResource Include="Assets\Audio\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
</ItemGroup>
|
</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