diff --git a/src/Agent/AgentManager.cs b/src/Agent/AgentManager.cs index fe04c00..469e4a7 100644 --- a/src/Agent/AgentManager.cs +++ b/src/Agent/AgentManager.cs @@ -597,6 +597,26 @@ public void Speak(string text) } } + /// + /// Makes the character speak text sentence by sentence + /// + public void SpeakSentences(List sentences) + { + EnsureLoaded(); + if (sentences == null || sentences.Count == 0) + return; + + // MS Agent Speak queues the text, so we can just call Speak for each sentence + // and it will display them sequentially in separate speech bubbles. + foreach (var sentence in sentences) + { + if (!string.IsNullOrEmpty(sentence)) + { + _character.Speak(sentence, null); + } + } + } + /// /// Makes the character think the specified text (shows in thought balloon) /// diff --git a/src/Config/AppSettings.cs b/src/Config/AppSettings.cs index cfbab83..d7d3795 100644 --- a/src/Config/AppSettings.cs +++ b/src/Config/AppSettings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Drawing; using System.IO; +using System.Text.RegularExpressions; using Newtonsoft.Json; namespace MSAgentAI.Config @@ -153,6 +154,9 @@ public class AppSettings // Agent size (100 = normal, 50 = half, 200 = double) public int AgentSize { get; set; } = 100; + // Speech truncation (sentence by sentence) + public bool TruncateSpeech { get; set; } = false; + // Idle animation spacing (in idle timer ticks - higher = less frequent) public int IdleAnimationSpacing { get; set; } = 5; @@ -249,15 +253,15 @@ public string ProcessText(string text) { // Use word boundaries (\b) to match WHOLE words only, not substrings // This prevents "AI" from matching inside "Entertaining" - string pattern = @"\b" + System.Text.RegularExpressions.Regex.Escape(entry.Key) + @"\b"; + string pattern = @"\b" + Regex.Escape(entry.Key) + @"\b"; // The \map\ command REPLACES the word with the pronunciation // Format: \map="anim-ay"="anime"\ (replaces the word entirely) - text = System.Text.RegularExpressions.Regex.Replace( + text = Regex.Replace( text, pattern, match => $"\\map=\"{entry.Value}\"=\"{match.Value}\"\\", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); + RegexOptions.IgnoreCase); } } } @@ -285,18 +289,45 @@ public static (string text, List animations) ExtractAnimationTriggers(st if (string.IsNullOrEmpty(text)) return (text, animations); - var matches = System.Text.RegularExpressions.Regex.Matches(text, @"&&(\w+)"); - foreach (System.Text.RegularExpressions.Match match in matches) + var matches = Regex.Matches(text, @"&&(\w+)"); + foreach (Match match in matches) { animations.Add(match.Groups[1].Value); } // Remove animation triggers from text - text = System.Text.RegularExpressions.Regex.Replace(text, @"&&\w+\s*", "").Trim(); + text = Regex.Replace(text, @"&&\w+\s*", "").Trim(); return (text, animations); } + /// + /// Splits text into sentences for sentence-by-sentence speech + /// + public static List SplitIntoSentences(string text) + { + var sentences = new List(); + if (string.IsNullOrEmpty(text)) + return sentences; + + // Split by common sentence endings: period, exclamation, question mark + // Also handle ellipsis (...) as a sentence boundary + // Note: This is a simple implementation that may not handle all edge cases + // (e.g., abbreviations like "Dr." or decimal numbers like "3.14") + var parts = Regex.Split(text, @"(?<=[.!?])\s+|(?<=\.\.\.)\s*"); + + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (!string.IsNullOrEmpty(trimmed)) + { + sentences.Add(trimmed); + } + } + + return sentences; + } + /// /// Gets a random line with text processing applied /// diff --git a/src/UI/ChatForm.cs b/src/UI/ChatForm.cs index ce833b8..a33fd9a 100644 --- a/src/UI/ChatForm.cs +++ b/src/UI/ChatForm.cs @@ -288,8 +288,16 @@ private void SpeakWithAnimations(string text, string defaultAnimation = null) _agentManager.PlayAnimation(defaultAnimation); } - // Speak the processed text - _agentManager.Speak(cleanText); + // Speak the processed text - check if truncation is enabled + if (_settings.TruncateSpeech) + { + var sentences = AppSettings.SplitIntoSentences(cleanText); + _agentManager.SpeakSentences(sentences); + } + else + { + _agentManager.Speak(cleanText); + } } private void AppendToHistory(string speaker, string message, Color color) diff --git a/src/UI/MainForm.cs b/src/UI/MainForm.cs index b36803d..c51433f 100644 --- a/src/UI/MainForm.cs +++ b/src/UI/MainForm.cs @@ -381,8 +381,16 @@ private void SpeakWithAnimations(string text, string defaultAnimation = null) _agentManager.PlayAnimation(defaultAnimation); } - // Speak the processed text - _agentManager.Speak(cleanText); + // Speak the processed text - check if truncation is enabled + if (_settings.TruncateSpeech) + { + var sentences = AppSettings.SplitIntoSentences(cleanText); + _agentManager.SpeakSentences(sentences); + } + else + { + _agentManager.Speak(cleanText); + } } /// diff --git a/src/UI/SettingsForm.cs b/src/UI/SettingsForm.cs index 9cf3a6c..b55d060 100644 --- a/src/UI/SettingsForm.cs +++ b/src/UI/SettingsForm.cs @@ -66,6 +66,7 @@ public class SettingsForm : Form private Label _confidenceValueLabel; private TrackBar _silenceTrackBar; private Label _silenceValueLabel; + private CheckBox _truncateSpeechCheckBox; // Ollama controls private TextBox _ollamaUrlTextBox; @@ -178,15 +179,15 @@ private void InitializeComponent() this.Text = "MSAgent AI Settings"; this.Size = new Size(650, 550); this.StartPosition = FormStartPosition.CenterScreen; - this.FormBorderStyle = FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; + this.FormBorderStyle = FormBorderStyle.Sizable; + this.MinimumSize = new Size(650, 550); // Create main tab control _tabControl = new TabControl { Location = new Point(10, 10), - Size = new Size(615, 450) + Size = new Size(615, 450), + Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right }; // Create tabs @@ -205,7 +206,8 @@ private void InitializeComponent() Text = "OK", Location = new Point(365, 470), Size = new Size(80, 30), - DialogResult = DialogResult.OK + DialogResult = DialogResult.OK, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _okButton.Click += OnOkClick; @@ -214,14 +216,16 @@ private void InitializeComponent() Text = "Cancel", Location = new Point(455, 470), Size = new Size(80, 30), - DialogResult = DialogResult.Cancel + DialogResult = DialogResult.Cancel, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _applyButton = new Button { Text = "Apply", Location = new Point(545, 470), - Size = new Size(80, 30) + Size = new Size(80, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _applyButton.Click += OnApplyClick; @@ -645,6 +649,14 @@ private void CreateVoiceTab() ForeColor = Color.Gray, Font = new Font(this.Font.FontFamily, 7.5f) }; + + // Truncate speech checkbox + _truncateSpeechCheckBox = new CheckBox + { + Text = "Sentence-by-sentence speech (truncate long speeches into separate bubbles)", + Location = new Point(15, 370), + Size = new Size(550, 25) + }; _voiceTab.Controls.AddRange(new Control[] { @@ -656,7 +668,8 @@ private void CreateVoiceTab() callModeLabel, micLabel, _microphoneComboBox, confidenceLabel, _confidenceTrackBar, _confidenceValueLabel, confidenceHint, - silenceLabel, _silenceTrackBar, _silenceValueLabel, silenceHint + silenceLabel, _silenceTrackBar, _silenceValueLabel, silenceHint, + _truncateSpeechCheckBox }); } @@ -1334,6 +1347,9 @@ private void LoadSettings() // Agent size _agentSizeTrackBar.Value = Math.Max(_agentSizeTrackBar.Minimum, Math.Min(_agentSizeTrackBar.Maximum, _settings.AgentSize)); _agentSizeValueLabel.Text = _agentSizeTrackBar.Value.ToString() + "%"; + + // Truncate speech + _truncateSpeechCheckBox.Checked = _settings.TruncateSpeech; // Ollama settings _ollamaUrlTextBox.Text = _settings.OllamaUrl; @@ -1431,6 +1447,9 @@ private void SaveSettings() // Agent size _settings.AgentSize = _agentSizeTrackBar.Value; + + // Truncate speech + _settings.TruncateSpeech = _truncateSpeechCheckBox.Checked; // Ollama settings _settings.OllamaUrl = _ollamaUrlTextBox.Text;