1
0

feat: Introduce ITranscriptionOrchestrator and related interfaces, refactoring DaemonService and other components to use dependency injection.

This commit is contained in:
2026-02-28 15:36:03 +01:00
parent ac0ac2397b
commit 0577640da9
18 changed files with 356 additions and 175 deletions

View File

@@ -5,10 +5,11 @@ 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;
using Toak.Core.Interfaces;
namespace Toak.Api; namespace Toak.Api;
public class GroqApiClient public class GroqApiClient : ISpeechClient, ILlmClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;

View File

@@ -3,15 +3,25 @@ using System.Diagnostics;
using Toak.Core; using Toak.Core;
using Toak.IO; using Toak.IO;
using Toak.Core.Interfaces;
namespace Toak.Audio; namespace Toak.Audio;
public static class AudioRecorder public class AudioRecorder : IAudioRecorder
{ {
private static readonly string WavPath = Path.Combine(Path.GetTempPath(), "toak_recording.wav"); private readonly string WavPath = Path.Combine(Path.GetTempPath(), "toak_recording.wav");
private readonly IRecordingStateTracker _stateTracker;
private readonly INotifications _notifications;
public static string GetWavPath() => WavPath; public AudioRecorder(IRecordingStateTracker stateTracker, INotifications notifications)
{
_stateTracker = stateTracker;
_notifications = notifications;
}
public static void StartRecording() public string GetWavPath() => WavPath;
public void StartRecording()
{ {
if (File.Exists(WavPath)) if (File.Exists(WavPath))
{ {
@@ -34,14 +44,14 @@ public static class AudioRecorder
var process = Process.Start(pInfo); var process = Process.Start(pInfo);
if (process != null) if (process != null)
{ {
StateTracker.SetRecording(process.Id); _stateTracker.SetRecording(process.Id);
Notifications.Notify("Recording Started"); _notifications.Notify("Recording Started");
} }
} }
public static void StopRecording() public void StopRecording()
{ {
var pid = StateTracker.GetRecordingPid(); var pid = _stateTracker.GetRecordingPid();
if (pid.HasValue) if (pid.HasValue)
{ {
Logger.LogDebug($"Found active recording process with PID {pid.Value}. Attempting to stop..."); Logger.LogDebug($"Found active recording process with PID {pid.Value}. Attempting to stop...");
@@ -69,7 +79,7 @@ public static class AudioRecorder
} }
finally finally
{ {
StateTracker.ClearRecording(); _stateTracker.ClearRecording();
} }
} }
} }

View File

@@ -9,7 +9,8 @@ public static class ConfigUpdaterCommand
public static async Task ExecuteAsync(string key, string val, bool verbose) public static async Task ExecuteAsync(string key, string val, bool verbose)
{ {
Toak.Core.Logger.Verbose = verbose; Toak.Core.Logger.Verbose = verbose;
var config = ConfigManager.LoadConfig(); var configManager = new ConfigManager();
var config = configManager.LoadConfig();
key = key.ToLowerInvariant(); key = key.ToLowerInvariant();
val = val.ToLowerInvariant(); val = val.ToLowerInvariant();
@@ -33,7 +34,7 @@ public static class ConfigUpdaterCommand
return; return;
} }
ConfigManager.SaveConfig(config); configManager.SaveConfig(config);
AnsiConsole.MarkupLine($"[green]Successfully[/] set {key} to [blue]{val}[/]."); AnsiConsole.MarkupLine($"[green]Successfully[/] set {key} to [blue]{val}[/].");
} }
} }

View File

@@ -15,14 +15,16 @@ public static class HistoryCommand
{ {
Logger.Verbose = verbose; Logger.Verbose = verbose;
var historyManager = new HistoryManager();
if (shred) if (shred)
{ {
HistoryManager.Shred(); historyManager.ClearHistory();
AnsiConsole.MarkupLine("[green]History successfully shredded.[/]"); AnsiConsole.MarkupLine("[green]History successfully shredded.[/]");
return; return;
} }
var entries = HistoryManager.LoadEntries(); var entries = historyManager.LoadHistory();
if (entries.Count == 0) if (entries.Count == 0)
{ {
AnsiConsole.MarkupLine("[yellow]No history found.[/]"); AnsiConsole.MarkupLine("[yellow]No history found.[/]");

View File

@@ -14,7 +14,7 @@ public static class LatencyTestCommand
public static async Task ExecuteAsync(bool verbose) public static async Task ExecuteAsync(bool verbose)
{ {
Logger.Verbose = verbose; Logger.Verbose = verbose;
var config = ConfigManager.LoadConfig(); var config = new ConfigManager().LoadConfig();
if (string.IsNullOrWhiteSpace(config.GroqApiKey)) if (string.IsNullOrWhiteSpace(config.GroqApiKey))
{ {
AnsiConsole.MarkupLine("[red]Groq API Key is not configured.[/] Run 'toak onboard'."); AnsiConsole.MarkupLine("[red]Groq API Key is not configured.[/] Run 'toak onboard'.");

View File

@@ -12,7 +12,8 @@ public static class OnboardCommand
public static async Task ExecuteAsync(bool verbose) public static async Task ExecuteAsync(bool verbose)
{ {
Toak.Core.Logger.Verbose = verbose; Toak.Core.Logger.Verbose = verbose;
var config = ConfigManager.LoadConfig(); var configManager = new ConfigManager();
var config = configManager.LoadConfig();
AnsiConsole.Write(new FigletText("Toak").Color(Color.Green)); AnsiConsole.Write(new FigletText("Toak").Color(Color.Green));
AnsiConsole.MarkupLine("[grey]Welcome to the Toak configuration wizard.[/]"); AnsiConsole.MarkupLine("[grey]Welcome to the Toak configuration wizard.[/]");
@@ -71,7 +72,7 @@ public static class OnboardCommand
.AddChoices(availableSkills)); .AddChoices(availableSkills));
} }
ConfigManager.SaveConfig(config); configManager.SaveConfig(config);
AnsiConsole.MarkupLine("\n[bold green]Configuration saved successfully![/]"); AnsiConsole.MarkupLine("\n[bold green]Configuration saved successfully![/]");
} }

View File

@@ -9,7 +9,7 @@ public static class ShowCommand
public static async Task ExecuteAsync(bool verbose) public static async Task ExecuteAsync(bool verbose)
{ {
Toak.Core.Logger.Verbose = verbose; Toak.Core.Logger.Verbose = verbose;
var config = ConfigManager.LoadConfig(); var config = new ConfigManager().LoadConfig();
var table = new Table(); var table = new Table();
table.AddColumn("Setting"); table.AddColumn("Setting");

View File

@@ -13,7 +13,7 @@ public static class StatsCommand
{ {
Logger.Verbose = verbose; Logger.Verbose = verbose;
var entries = HistoryManager.LoadEntries(); var entries = new HistoryManager().LoadHistory();
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.[/]");

View File

@@ -2,14 +2,21 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Toak.Serialization; using Toak.Serialization;
using Toak.Core.Interfaces;
namespace Toak.Configuration; namespace Toak.Configuration;
public static class ConfigManager
{
private static readonly string ConfigDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "toak");
private static readonly string ConfigPath = Path.Combine(ConfigDir, "config.json");
public static ToakConfig LoadConfig() public class ConfigManager : IConfigProvider
{
private readonly string ConfigDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "toak");
private readonly string ConfigPath;
public ConfigManager()
{
ConfigPath = Path.Combine(ConfigDir, "config.json");
}
public ToakConfig LoadConfig()
{ {
if (!File.Exists(ConfigPath)) if (!File.Exists(ConfigPath))
{ {
@@ -27,7 +34,7 @@ public static class ConfigManager
} }
} }
public static void SaveConfig(ToakConfig config) public void SaveConfig(ToakConfig config)
{ {
if (!Directory.Exists(ConfigDir)) if (!Directory.Exists(ConfigDir))
{ {

View File

@@ -2,19 +2,17 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Toak.Audio;
using Toak.Configuration; using Toak.Configuration;
using Toak.Api; using Toak.Api;
using Toak.Core.Interfaces;
using Toak.Audio;
using Toak.IO; using Toak.IO;
namespace Toak.Core; namespace Toak.Core;
public static class DaemonService public static class DaemonService
{ {
private static GroqApiClient? _groqClient;
public static string GetSocketPath() public static string GetSocketPath()
{ {
var runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); var runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
@@ -49,14 +47,29 @@ public static class DaemonService
try { File.Delete(socketPath); } catch { } try { File.Delete(socketPath); } catch { }
} }
var config = ConfigManager.LoadConfig(); var configManager = new ConfigManager();
var config = configManager.LoadConfig();
if (string.IsNullOrWhiteSpace(config.GroqApiKey)) if (string.IsNullOrWhiteSpace(config.GroqApiKey))
{ {
Console.WriteLine("Groq API Key is not configured. Run 'toak onboard'."); Console.WriteLine("Groq API Key is not configured. Run 'toak onboard'.");
return; return;
} }
_groqClient = new GroqApiClient(config.GroqApiKey); var stateTracker = new StateTracker();
var notifications = new Notifications();
var groqClient = new GroqApiClient(config.GroqApiKey);
var orchestrator = new TranscriptionOrchestrator(
groqClient,
groqClient,
configManager,
new AudioRecorder(stateTracker, notifications),
notifications,
new TextInjector(notifications),
new HistoryManager(),
new ClipboardManager(notifications),
stateTracker
);
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
var endPoint = new UnixDomainSocketEndPoint(socketPath); var endPoint = new UnixDomainSocketEndPoint(socketPath);
@@ -71,7 +84,7 @@ public static class DaemonService
while (true) while (true)
{ {
var client = await socket.AcceptAsync(); var client = await socket.AcceptAsync();
_ = Task.Run(() => HandleClientAsync(client)); _ = Task.Run(() => HandleClientAsync(client, orchestrator, stateTracker));
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -87,7 +100,7 @@ public static class DaemonService
} }
} }
private static async Task HandleClientAsync(Socket client) private static async Task HandleClientAsync(Socket client, ITranscriptionOrchestrator orchestrator, IRecordingStateTracker stateTracker)
{ {
try try
{ {
@@ -101,22 +114,22 @@ public static class DaemonService
if (cmd == 1) // START if (cmd == 1) // START
{ {
await ProcessStartRecordingAsync(); await orchestrator.ProcessStartRecordingAsync();
} }
else if (cmd == 2) // STOP else if (cmd == 2) // STOP
{ {
await ProcessStopRecordingAsync(client, pipeToStdout, copyToClipboard); await orchestrator.ProcessStopRecordingAsync(client, pipeToStdout, copyToClipboard);
} }
else if (cmd == 3) // ABORT else if (cmd == 3) // ABORT
{ {
ProcessAbortAsync(); orchestrator.ProcessAbortAsync();
} }
else if (cmd == 4) // TOGGLE else if (cmd == 4) // TOGGLE
{ {
if (StateTracker.IsRecording()) if (stateTracker.IsRecording())
await ProcessStopRecordingAsync(client, pipeToStdout, copyToClipboard); await orchestrator.ProcessStopRecordingAsync(client, pipeToStdout, copyToClipboard);
else else
await ProcessStartRecordingAsync(); await orchestrator.ProcessStartRecordingAsync();
} }
} }
} }
@@ -129,114 +142,5 @@ public static class DaemonService
client.Close(); client.Close();
} }
} }
private static async Task ProcessStartRecordingAsync()
{
if (StateTracker.IsRecording()) return;
Logger.LogDebug("Received START command");
var config = ConfigManager.LoadConfig();
Notifications.PlaySound(config.StartSoundPath);
AudioRecorder.StartRecording();
} }
private static async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard)
{
if (!StateTracker.IsRecording()) return;
Logger.LogDebug("Received STOP command");
var config = ConfigManager.LoadConfig();
Notifications.PlaySound(config.StopSoundPath);
Notifications.Notify("Toak", "Transcribing...");
AudioRecorder.StopRecording();
var wavPath = AudioRecorder.GetWavPath();
if (!File.Exists(wavPath) || new FileInfo(wavPath).Length == 0)
{
Notifications.Notify("Toak", "No audio recorded.");
return;
}
try
{
var stopWatch = Stopwatch.StartNew();
Logger.LogDebug($"Starting STT via Whisper for {wavPath}...");
var transcript = await _groqClient!.TranscribeAsync(wavPath, config.WhisperLanguage, config.WhisperModel);
if (string.IsNullOrWhiteSpace(transcript))
{
Notifications.Notify("Toak", "No speech detected.");
return;
}
// LLM Refinement
var detectedSkill = Toak.Core.Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills);
string systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config);
bool isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution;
if (isExecutionSkill)
{
var finalText = await _groqClient.RefineTextAsync(transcript, systemPrompt, config.LlmModel);
if (!string.IsNullOrWhiteSpace(finalText))
{
detectedSkill!.Execute(finalText);
stopWatch.Stop();
HistoryManager.SaveEntry(transcript, finalText, detectedSkill.Name, stopWatch.ElapsedMilliseconds);
Notifications.Notify("Toak", $"Skill executed in {stopWatch.ElapsedMilliseconds}ms");
}
}
else
{
Logger.LogDebug("Starting LLM text refinement (streaming)...");
var tokenStream = _groqClient.RefineTextStreamAsync(transcript, systemPrompt, config.LlmModel);
if (pipeToStdout || copyToClipboard)
{
string fullText = "";
await foreach (var token in tokenStream)
{
fullText += token;
if (pipeToStdout)
{
await client.SendAsync(System.Text.Encoding.UTF8.GetBytes(token), SocketFlags.None);
}
}
stopWatch.Stop();
if (copyToClipboard)
{
ClipboardManager.Copy(fullText);
Notifications.Notify("Toak", $"Copied to clipboard in {stopWatch.ElapsedMilliseconds}ms");
}
HistoryManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds);
}
else
{
string fullText = await TextInjector.InjectStreamAsync(tokenStream, config.TypingBackend);
stopWatch.Stop();
HistoryManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds);
Notifications.Notify("Toak", $"Done in {stopWatch.ElapsedMilliseconds}ms");
}
}
}
catch (Exception ex)
{
Notifications.Notify("Toak Error", ex.Message);
Logger.LogDebug($"Error during processing: {ex.Message}");
}
finally
{
if (File.Exists(wavPath)) File.Delete(wavPath);
}
}
private static void ProcessAbortAsync()
{
Logger.LogDebug("Received ABORT command");
AudioRecorder.StopRecording();
var wavPath = AudioRecorder.GetWavPath();
if (File.Exists(wavPath)) File.Delete(wavPath);
Notifications.Notify("Toak", "Recording Aborted.");
}
}

View File

@@ -4,15 +4,21 @@ using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Toak.Serialization; using Toak.Serialization;
using Toak.Core.Interfaces;
namespace Toak.Core; namespace Toak.Core;
public static class HistoryManager public class HistoryManager : IHistoryManager
{ {
private static readonly string HistoryDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "toak"); private readonly string HistoryDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "toak");
private static readonly string HistoryFile = Path.Combine(HistoryDir, "history.jsonl"); private readonly string HistoryFile;
public static void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs) public HistoryManager()
{
HistoryFile = Path.Combine(HistoryDir, "history.jsonl");
}
public void SaveEntry(string rawTranscript, string refinedText, string? skillName, long durationMs)
{ {
try try
{ {
@@ -44,7 +50,7 @@ public static class HistoryManager
} }
} }
public static List<HistoryEntry> LoadEntries() 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;
@@ -85,7 +91,7 @@ public static class HistoryManager
return entries; return entries;
} }
public static void Shred() public void ClearHistory()
{ {
if (File.Exists(HistoryFile)) if (File.Exists(HistoryFile))
{ {

View File

@@ -0,0 +1,11 @@
using System.Net.Sockets;
using System.Threading.Tasks;
namespace Toak.Core.Interfaces;
public interface ITranscriptionOrchestrator
{
Task ProcessStartRecordingAsync();
Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard);
void ProcessAbortAsync();
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Toak.Configuration;
namespace Toak.Core.Interfaces;
public interface IConfigProvider
{
ToakConfig LoadConfig();
void SaveConfig(ToakConfig config);
}
public interface ISpeechClient
{
Task<string> TranscribeAsync(string filePath, string language = "", string model = "whisper-large-v3-turbo");
}
public interface ILlmClient
{
Task<string> RefineTextAsync(string rawTranscript, string systemPrompt, string model = "openai/gpt-oss-20b");
IAsyncEnumerable<string> RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = "openai/gpt-oss-20b");
}
public interface IAudioRecorder
{
string GetWavPath();
void StartRecording();
void StopRecording();
}
public interface INotifications
{
void Notify(string title, string message = "");
void PlaySound(string soundPath);
}
public interface ITextInjector
{
Task<string> InjectStreamAsync(IAsyncEnumerable<string> textStream, string backend = "xdotool");
Task InjectTextAsync(string text, string backend = "xdotool");
}
public interface IHistoryManager
{
void SaveEntry(string rawText, string finalText, string? skillUsed, long timeTakenMs);
List<HistoryEntry> LoadHistory();
void ClearHistory();
}
public interface IClipboardManager
{
void Copy(string text);
}
public interface IRecordingStateTracker
{
int? GetRecordingPid();
void SetRecording(int pid);
void ClearRecording();
bool IsRecording();
}

View File

@@ -1,21 +1,23 @@
using Toak.Core.Interfaces;
namespace Toak.Core; namespace Toak.Core;
public static class StateTracker public class StateTracker : IRecordingStateTracker
{ {
private static readonly string StateFilePath = Path.Combine(Path.GetTempPath(), "toak_state.pid"); private readonly string StateFilePath = Path.Combine(Path.GetTempPath(), "toak_state.pid");
public static bool IsRecording() public bool IsRecording()
{ {
return File.Exists(StateFilePath); return File.Exists(StateFilePath);
} }
public static 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.ToString()); File.WriteAllText(StateFilePath, ffmpegPid.ToString());
} }
public static int? GetRecordingPid() public int? GetRecordingPid()
{ {
if (File.Exists(StateFilePath)) if (File.Exists(StateFilePath))
{ {
@@ -29,7 +31,7 @@ public static class StateTracker
return null; return null;
} }
public static void ClearRecording() public void ClearRecording()
{ {
if (File.Exists(StateFilePath)) if (File.Exists(StateFilePath))
{ {

View File

@@ -0,0 +1,153 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
using Toak.Core.Interfaces;
using Toak.Configuration;
namespace Toak.Core;
public class TranscriptionOrchestrator : ITranscriptionOrchestrator
{
private readonly ISpeechClient _speechClient;
private readonly ILlmClient _llmClient;
private readonly IConfigProvider _configProvider;
private readonly IAudioRecorder _audioRecorder;
private readonly INotifications _notifications;
private readonly ITextInjector _textInjector;
private readonly IHistoryManager _historyManager;
private readonly IClipboardManager _clipboardManager;
private readonly IRecordingStateTracker _stateTracker;
public TranscriptionOrchestrator(
ISpeechClient speechClient,
ILlmClient llmClient,
IConfigProvider configProvider,
IAudioRecorder audioRecorder,
INotifications notifications,
ITextInjector textInjector,
IHistoryManager historyManager,
IClipboardManager clipboardManager,
IRecordingStateTracker stateTracker)
{
_speechClient = speechClient;
_llmClient = llmClient;
_configProvider = configProvider;
_audioRecorder = audioRecorder;
_notifications = notifications;
_textInjector = textInjector;
_historyManager = historyManager;
_clipboardManager = clipboardManager;
_stateTracker = stateTracker;
}
public async Task ProcessStartRecordingAsync()
{
if (_stateTracker.IsRecording()) return;
Logger.LogDebug("Received START command");
var config = _configProvider.LoadConfig();
_notifications.PlaySound(config.StartSoundPath);
_audioRecorder.StartRecording();
}
public async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard)
{
if (!_stateTracker.IsRecording()) return;
Logger.LogDebug("Received STOP command");
var config = _configProvider.LoadConfig();
_notifications.PlaySound(config.StopSoundPath);
_notifications.Notify("Toak", "Transcribing...");
_audioRecorder.StopRecording();
var wavPath = _audioRecorder.GetWavPath();
if (!File.Exists(wavPath) || new FileInfo(wavPath).Length == 0)
{
_notifications.Notify("Toak", "No audio recorded.");
return;
}
try
{
var stopWatch = Stopwatch.StartNew();
Logger.LogDebug($"Starting STT via Whisper for {wavPath}...");
var transcript = await _speechClient.TranscribeAsync(wavPath, config.WhisperLanguage, config.WhisperModel);
if (string.IsNullOrWhiteSpace(transcript))
{
_notifications.Notify("Toak", "No speech detected.");
return;
}
var detectedSkill = Toak.Core.Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills);
string systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config);
bool isExecutionSkill = detectedSkill != null && detectedSkill.HandlesExecution;
if (isExecutionSkill)
{
var finalText = await _llmClient.RefineTextAsync(transcript, systemPrompt, config.LlmModel);
if (!string.IsNullOrWhiteSpace(finalText))
{
detectedSkill!.Execute(finalText);
stopWatch.Stop();
_historyManager.SaveEntry(transcript, finalText, detectedSkill.Name, stopWatch.ElapsedMilliseconds);
_notifications.Notify("Toak", $"Skill executed in {stopWatch.ElapsedMilliseconds}ms");
}
}
else
{
Logger.LogDebug("Starting LLM text refinement (streaming)...");
var tokenStream = _llmClient.RefineTextStreamAsync(transcript, systemPrompt, config.LlmModel);
if (pipeToStdout || copyToClipboard)
{
string fullText = "";
await foreach (var token in tokenStream)
{
fullText += token;
if (pipeToStdout)
{
await client.SendAsync(System.Text.Encoding.UTF8.GetBytes(token), SocketFlags.None);
}
}
stopWatch.Stop();
if (copyToClipboard)
{
_clipboardManager.Copy(fullText);
_notifications.Notify("Toak", $"Copied to clipboard in {stopWatch.ElapsedMilliseconds}ms");
}
_historyManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds);
}
else
{
string fullText = await _textInjector.InjectStreamAsync(tokenStream, config.TypingBackend);
stopWatch.Stop();
_historyManager.SaveEntry(transcript, fullText, detectedSkill?.Name, stopWatch.ElapsedMilliseconds);
_notifications.Notify("Toak", $"Done in {stopWatch.ElapsedMilliseconds}ms");
}
}
}
catch (Exception ex)
{
_notifications.Notify("Toak Error", ex.Message);
Logger.LogDebug($"Error during processing: {ex.Message}");
}
finally
{
if (File.Exists(wavPath)) File.Delete(wavPath);
}
}
public void ProcessAbortAsync()
{
Logger.LogDebug("Received ABORT command");
_audioRecorder.StopRecording();
var wavPath = _audioRecorder.GetWavPath();
if (File.Exists(wavPath)) File.Delete(wavPath);
_notifications.Notify("Toak", "Recording Aborted.");
}
}

View File

@@ -1,10 +1,19 @@
using System.Diagnostics; using System.Diagnostics;
using Toak.Core.Interfaces;
namespace Toak.IO; namespace Toak.IO;
public static class ClipboardManager public class ClipboardManager : IClipboardManager
{ {
public static void Copy(string text) private readonly INotifications _notifications;
public ClipboardManager(INotifications notifications)
{
_notifications = notifications;
}
public void Copy(string text)
{ {
if (string.IsNullOrWhiteSpace(text)) return; if (string.IsNullOrWhiteSpace(text)) return;
try try
@@ -47,7 +56,7 @@ public static class ClipboardManager
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[ClipboardManager] Error copying text: {ex.Message}"); Console.WriteLine($"[ClipboardManager] Error copying text: {ex.Message}");
Notifications.Notify("Clipboard Error", "Could not copy text to clipboard."); _notifications.Notify("Clipboard Error", "Could not copy text to clipboard.");
} }
} }
} }

View File

@@ -1,12 +1,14 @@
using System.Diagnostics; using System.Diagnostics;
using Toak.Core.Interfaces;
namespace Toak.IO; namespace Toak.IO;
public static class Notifications public class Notifications : INotifications
{ {
private static bool _notifySendAvailable = true; private bool _notifySendAvailable = true;
public static void Notify(string summary, string body = "") public void Notify(string summary, string body = "")
{ {
if (!_notifySendAvailable) return; if (!_notifySendAvailable) return;
@@ -32,9 +34,9 @@ public static class Notifications
} }
} }
private static bool _paplayAvailable = true; private bool _paplayAvailable = true;
public static void PlaySound(string soundPath) public void PlaySound(string soundPath)
{ {
if (!_paplayAvailable || string.IsNullOrWhiteSpace(soundPath)) return; if (!_paplayAvailable || string.IsNullOrWhiteSpace(soundPath)) return;
try try

View File

@@ -2,14 +2,23 @@ using System.Diagnostics;
using Toak.Core; using Toak.Core;
using Toak.Core.Interfaces;
namespace Toak.IO; namespace Toak.IO;
public static class TextInjector public class TextInjector : ITextInjector
{ {
public static void Inject(string text, string backend) private readonly INotifications _notifications;
public TextInjector(INotifications notifications)
{
_notifications = notifications;
}
public Task InjectTextAsync(string text, string backend = "xdotool")
{ {
Logger.LogDebug($"Injecting text: '{text}' with {backend}"); Logger.LogDebug($"Injecting text: '{text}' with {backend}");
if (string.IsNullOrWhiteSpace(text)) return; if (string.IsNullOrWhiteSpace(text)) return Task.CompletedTask;
try try
{ {
@@ -42,11 +51,12 @@ public static class TextInjector
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[TextInjector] Error injecting text: {ex.Message}"); Console.WriteLine($"[TextInjector] Error injecting text: {ex.Message}");
Notifications.Notify("Injection Error", "Could not type text into window."); _notifications.Notify("Injection Error", "Could not type text into window.");
} }
return Task.CompletedTask;
} }
public static async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream, string backend) public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream, string backend)
{ {
string fullText = string.Empty; string fullText = string.Empty;
try try
@@ -97,7 +107,7 @@ public static class TextInjector
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[TextInjector] Error injecting text stream: {ex.Message}"); Console.WriteLine($"[TextInjector] Error injecting text stream: {ex.Message}");
Notifications.Notify("Injection Error", "Could not type text stream into window."); _notifications.Notify("Injection Error", "Could not type text stream into window.");
} }
return fullText; return fullText;
} }