using System.Diagnostics; using System.Net.Sockets; using Toak.Core.Interfaces; namespace Toak.Core; 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 = speechClient; private readonly ILlmClient _llmClient = llmClient; private readonly IConfigProvider _configProvider = configProvider; private readonly IAudioRecorder _audioRecorder = audioRecorder; private readonly INotifications _notifications = notifications; private readonly ITextInjector _textInjector = textInjector; private readonly IHistoryManager _historyManager = historyManager; private readonly IClipboardManager _clipboardManager = clipboardManager; private readonly IRecordingStateTracker _stateTracker = stateTracker; public Task ProcessStartRecordingAsync() { if (_stateTracker.IsRecording()) return Task.CompletedTask; Logger.LogDebug("Received START command"); var config = _configProvider.LoadConfig(); _notifications.PlaySound(config.StartSoundPath); _audioRecorder.StartRecording(); return Task.CompletedTask; } public async Task ProcessStopRecordingAsync(Socket client, bool pipeToStdout, bool copyToClipboard) { if (!_stateTracker.IsRecording()) return; Logger.LogDebug("Received STOP command"); var config = _configProvider.LoadConfig(); var startTime = _stateTracker.GetRecordingStartTime(); if (startTime.HasValue) { var duration = (DateTime.UtcNow - startTime.Value).TotalMilliseconds; if (duration < config.MinRecordingDuration) { Logger.LogDebug($"Recording duration {duration}ms is less than min {config.MinRecordingDuration}ms. Discarding."); ProcessAbortAsync(); return; } } _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 = Skills.SkillRegistry.DetectSkill(transcript, config.ActiveSkills); var systemPrompt = detectedSkill != null ? detectedSkill.GetSystemPrompt(transcript) : PromptBuilder.BuildPrompt(config); var 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) { var 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 { var fullText = await _textInjector.InjectStreamAsync(tokenStream); 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."); } }