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;