v0.8: TCP transport via gatuna tunnel, session IDs, pre-synth playback, server Rust rewrite
This commit is contained in:
@@ -12,7 +12,7 @@ public sealed class AppConfig
|
||||
public int LengthScale { get; set; } = 100;
|
||||
public int NoiseWScale { get; set; } = 800;
|
||||
public bool MinimizeToTray { get; set; } = true;
|
||||
public string InterfaceIp { get; set; } = string.Empty;
|
||||
public string SttEndpoint { get; set; } = "127.0.0.1:6996";
|
||||
|
||||
public static string AppDataDir => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
|
||||
Generated
+8
-9
@@ -17,7 +17,7 @@ partial class MainForm
|
||||
private Button btnBrowseFile = null!;
|
||||
private Label lblFileName = null!;
|
||||
private Label lblServer = null!;
|
||||
private ComboBox cmbInterface = null!;
|
||||
private TextBox txtSttEndpoint = null!;
|
||||
private Label lblLineStatus = null!;
|
||||
private RichTextBox txtLog = null!;
|
||||
private CheckBox chkMinimizeToTray = null!;
|
||||
@@ -57,7 +57,7 @@ partial class MainForm
|
||||
btnBrowseFile = new Button();
|
||||
lblFileName = new Label();
|
||||
lblServer = new Label();
|
||||
cmbInterface = new ComboBox();
|
||||
txtSttEndpoint = new TextBox();
|
||||
lblLineStatus = new Label();
|
||||
txtLog = new RichTextBox();
|
||||
chkMinimizeToTray = new CheckBox();
|
||||
@@ -143,15 +143,14 @@ partial class MainForm
|
||||
lblFileName.ForeColor = Color.Gray;
|
||||
|
||||
// lblServer
|
||||
lblServer.Text = "Interface:";
|
||||
lblServer.Text = "STT:";
|
||||
lblServer.Location = new Point(440, 48);
|
||||
lblServer.Size = new Size(55, 23);
|
||||
lblServer.Size = new Size(35, 23);
|
||||
lblServer.TextAlign = ContentAlignment.MiddleLeft;
|
||||
|
||||
// cmbInterface
|
||||
cmbInterface.Location = new Point(498, 45);
|
||||
cmbInterface.Size = new Size(165, 23);
|
||||
cmbInterface.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
// txtSttEndpoint
|
||||
txtSttEndpoint.Location = new Point(478, 45);
|
||||
txtSttEndpoint.Size = new Size(185, 23);
|
||||
|
||||
// lblLineStatus
|
||||
lblLineStatus.Text = "";
|
||||
@@ -270,7 +269,7 @@ partial class MainForm
|
||||
Controls.Add(btnBrowseFile);
|
||||
Controls.Add(lblFileName);
|
||||
Controls.Add(lblServer);
|
||||
Controls.Add(cmbInterface);
|
||||
Controls.Add(txtSttEndpoint);
|
||||
Controls.Add(lblLineStatus);
|
||||
Controls.Add(lblNoise);
|
||||
Controls.Add(trkNoise);
|
||||
|
||||
+69
-101
@@ -2,7 +2,7 @@ using NAudio.Wave;
|
||||
using Robovoice.App;
|
||||
using Robovoice.Core;
|
||||
using Robovoice.Core.Voices;
|
||||
using Robovoice.Stt.Dhcp;
|
||||
using Robovoice.Stt.Tcp;
|
||||
using Robovoice.Tts.LibPiper;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -16,7 +16,7 @@ internal sealed partial class MainForm : Form
|
||||
|
||||
private LibPiperTtsEngine? _tts;
|
||||
private AudioOutput? _audioOutput;
|
||||
private DhcpSttSource? _sttSource;
|
||||
private TcpSttSource? _sttSource;
|
||||
private Orchestrator? _orchestrator;
|
||||
private PttHotkey? _pttHotkey;
|
||||
private NotifyIcon? _trayIcon;
|
||||
@@ -36,57 +36,64 @@ internal sealed partial class MainForm : Form
|
||||
|
||||
private async void OnLoad(object? sender, EventArgs e)
|
||||
{
|
||||
ActiveControl = txtLog;
|
||||
PopulateVoices();
|
||||
PopulateOutputDevices();
|
||||
try
|
||||
{
|
||||
ActiveControl = txtLog;
|
||||
PopulateVoices();
|
||||
PopulateOutputDevices();
|
||||
|
||||
_pttKey = (Keys)_config.PttKey;
|
||||
if (_pttKey == Keys.None)
|
||||
_pttKey = Keys.F8;
|
||||
txtPttKey.Text = KeyToDisplayString(_pttKey);
|
||||
_pttKey = (Keys)_config.PttKey;
|
||||
if (_pttKey == Keys.None)
|
||||
_pttKey = Keys.F8;
|
||||
txtPttKey.Text = KeyToDisplayString(_pttKey);
|
||||
|
||||
if (!string.IsNullOrEmpty(_config.Voice) && cmbVoice.Items.Contains(_config.Voice))
|
||||
cmbVoice.SelectedItem = _config.Voice;
|
||||
else if (cmbVoice.Items.Count > 0)
|
||||
cmbVoice.SelectedIndex = 0;
|
||||
if (!string.IsNullOrEmpty(_config.Voice) && cmbVoice.Items.Contains(_config.Voice))
|
||||
cmbVoice.SelectedItem = _config.Voice;
|
||||
else if (cmbVoice.Items.Count > 0)
|
||||
cmbVoice.SelectedIndex = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(_config.OutputDevice) && cmbOutput.Items.Contains(_config.OutputDevice))
|
||||
cmbOutput.SelectedItem = _config.OutputDevice;
|
||||
else
|
||||
AutoSelectCableOutput();
|
||||
if (!string.IsNullOrEmpty(_config.OutputDevice) && cmbOutput.Items.Contains(_config.OutputDevice))
|
||||
cmbOutput.SelectedItem = _config.OutputDevice;
|
||||
else
|
||||
AutoSelectCableOutput();
|
||||
|
||||
trkNoise.Value = _config.NoiseScale;
|
||||
trkSpeed.Value = _config.LengthScale;
|
||||
trkNoiseW.Value = _config.NoiseWScale;
|
||||
OnSliderScroll(null, EventArgs.Empty);
|
||||
trkNoise.Value = _config.NoiseScale;
|
||||
trkSpeed.Value = _config.LengthScale;
|
||||
trkNoiseW.Value = _config.NoiseWScale;
|
||||
OnSliderScroll(null, EventArgs.Empty);
|
||||
|
||||
chkMinimizeToTray.Checked = _config.MinimizeToTray;
|
||||
PopulateInterfaces();
|
||||
chkMinimizeToTray.Checked = _config.MinimizeToTray;
|
||||
txtSttEndpoint.Text = _config.SttEndpoint;
|
||||
|
||||
btnBrowseFile.Click += OnBrowseFile;
|
||||
btnTestVoice.Click += OnTestVoice;
|
||||
btnManageVoices.Click += OnManageVoices;
|
||||
btnClearLog.Click += (_, _) => txtLog.Clear();
|
||||
txtPttKey.Enter += OnPttKeyFocus;
|
||||
txtPttKey.KeyDown += OnPttKeyDown;
|
||||
cmbOutput.SelectedIndexChanged += OnOutputChanged;
|
||||
cmbVoice.SelectedIndexChanged += OnVoiceChanged;
|
||||
cmbInterface.SelectedIndexChanged += OnInterfaceChanged;
|
||||
btnBrowseFile.Click += OnBrowseFile;
|
||||
btnTestVoice.Click += OnTestVoice;
|
||||
btnManageVoices.Click += OnManageVoices;
|
||||
btnClearLog.Click += (_, _) => txtLog.Clear();
|
||||
txtPttKey.Enter += OnPttKeyFocus;
|
||||
txtPttKey.KeyDown += OnPttKeyDown;
|
||||
cmbOutput.SelectedIndexChanged += OnOutputChanged;
|
||||
cmbVoice.SelectedIndexChanged += OnVoiceChanged;
|
||||
txtSttEndpoint.Leave += OnSttEndpointChanged;
|
||||
|
||||
trkNoise.Scroll += OnSliderScroll;
|
||||
trkSpeed.Scroll += OnSliderScroll;
|
||||
trkNoiseW.Scroll += OnSliderScroll;
|
||||
trkNoise.MouseUp += OnSliderReleased;
|
||||
trkSpeed.MouseUp += OnSliderReleased;
|
||||
trkNoiseW.MouseUp += OnSliderReleased;
|
||||
trkNoise.Scroll += OnSliderScroll;
|
||||
trkSpeed.Scroll += OnSliderScroll;
|
||||
trkNoiseW.Scroll += OnSliderScroll;
|
||||
trkNoise.MouseUp += OnSliderReleased;
|
||||
trkSpeed.MouseUp += OnSliderReleased;
|
||||
trkNoiseW.MouseUp += OnSliderReleased;
|
||||
|
||||
chkMinimizeToTray.CheckedChanged += (_, _) => SaveConfig();
|
||||
chkMinimizeToTray.CheckedChanged += (_, _) => SaveConfig();
|
||||
|
||||
Resize += OnResize;
|
||||
Resize += OnResize;
|
||||
|
||||
SetupTray();
|
||||
SetupTray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"OnLoad failed: {ex}");
|
||||
}
|
||||
|
||||
await InitializeEngineAsync();
|
||||
BeginInvoke(async () => await InitializeEngineAsync());
|
||||
}
|
||||
|
||||
private Keys _pttKey = Keys.F8;
|
||||
@@ -232,7 +239,8 @@ internal sealed partial class MainForm : Form
|
||||
|
||||
Log($"Loading voice: {voiceName}...");
|
||||
_audioOutput?.Dispose();
|
||||
_tts?.DisposeAsync().AsTask().Wait();
|
||||
if (_tts is not null)
|
||||
await _tts.DisposeAsync();
|
||||
|
||||
_tts = new LibPiperTtsEngine(
|
||||
modelPath,
|
||||
@@ -242,21 +250,21 @@ internal sealed partial class MainForm : Form
|
||||
noiseWScale: trkNoiseW.Value / 1000.0f);
|
||||
_audioOutput = new AudioOutput();
|
||||
|
||||
if (_sttSource is null)
|
||||
{
|
||||
string ifaceIp = cmbInterface.SelectedItem as string ?? "";
|
||||
_sttSource = new DhcpSttSource { InterfaceIp = ifaceIp, Log = Log };
|
||||
await _sttSource.StartAsync();
|
||||
}
|
||||
|
||||
_orchestrator?.DisposeAsync().AsTask().Wait();
|
||||
_orchestrator = new Orchestrator(_tts, _audioOutput, _sttSource, Log)
|
||||
{
|
||||
OutputDeviceName = cmbOutput.SelectedItem as string ?? string.Empty,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (_sttSource is null)
|
||||
{
|
||||
_sttSource = new TcpSttSource { Endpoint = txtSttEndpoint.Text, Log = Log };
|
||||
await _sttSource.StartAsync();
|
||||
}
|
||||
|
||||
if (_orchestrator is not null)
|
||||
await _orchestrator.DisposeAsync();
|
||||
_orchestrator = new Orchestrator(_tts, _audioOutput, _sttSource, Log)
|
||||
{
|
||||
OutputDeviceName = cmbOutput.SelectedItem as string ?? string.Empty,
|
||||
};
|
||||
|
||||
await _orchestrator.InitializeTtsAsync();
|
||||
Log("Engine ready. Press PTT to send to STT server.");
|
||||
SetupHotkey();
|
||||
@@ -426,59 +434,19 @@ internal sealed partial class MainForm : Form
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
private void PopulateInterfaces()
|
||||
private async void OnSttEndpointChanged(object? sender, EventArgs e)
|
||||
{
|
||||
cmbInterface.Items.Clear();
|
||||
foreach (var (ip, name) in DhcpSttSource.GetAvailableInterfaces())
|
||||
{
|
||||
cmbInterface.Items.Add(name);
|
||||
cmbInterface.Items[^1] = name;
|
||||
cmbInterface.Items[cmbInterface.Items.Count - 1] = name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_config.InterfaceIp))
|
||||
{
|
||||
for (int i = 0; i < cmbInterface.Items.Count; i++)
|
||||
{
|
||||
if (cmbInterface.Items[i] is string s && s.Contains(_config.InterfaceIp))
|
||||
{
|
||||
cmbInterface.SelectedIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cmbInterface.Items.Count > 0)
|
||||
cmbInterface.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void OnInterfaceChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (cmbInterface.SelectedItem is not string selected)
|
||||
return;
|
||||
|
||||
string? ip = ExtractIpFromDisplay(selected);
|
||||
if (ip is null) return;
|
||||
|
||||
_config.InterfaceIp = ip;
|
||||
_config.SttEndpoint = txtSttEndpoint.Text;
|
||||
SaveConfig();
|
||||
|
||||
if (_sttSource is not null)
|
||||
{
|
||||
_sttSource.DisposeAsync().AsTask().Wait(2000);
|
||||
await _sttSource.DisposeAsync();
|
||||
_sttSource = null;
|
||||
_ = InitializeEngineAsync();
|
||||
await InitializeEngineAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ExtractIpFromDisplay(string display)
|
||||
{
|
||||
int start = display.IndexOf('(');
|
||||
int end = display.IndexOf(')');
|
||||
if (start < 0 || end <= start) return null;
|
||||
return display[(start + 1)..end];
|
||||
}
|
||||
|
||||
private void SetupTray()
|
||||
{
|
||||
if (_trayInit) return;
|
||||
@@ -544,7 +512,7 @@ internal sealed partial class MainForm : Form
|
||||
_config.LengthScale = trkSpeed.Value;
|
||||
_config.NoiseWScale = trkNoiseW.Value;
|
||||
_config.MinimizeToTray = chkMinimizeToTray.Checked;
|
||||
_config.InterfaceIp = ExtractIpFromDisplay(cmbInterface.SelectedItem as string ?? "") ?? "";
|
||||
_config.SttEndpoint = txtSttEndpoint.Text;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
private bool _playing;
|
||||
private Task? _synthTask;
|
||||
private CancellationTokenSource? _synthCts;
|
||||
private System.Threading.Timer? _segmentTimer;
|
||||
private static readonly TimeSpan SegmentTimeout = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
public string OutputDeviceName { get; set; } = string.Empty;
|
||||
|
||||
@@ -51,7 +49,6 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
_log($"SEGMENT: \"{msg.Text}\" ({msg.Text.Length} chars)");
|
||||
_pendingTexts.Enqueue(msg.Text);
|
||||
EnsureSynthTask();
|
||||
ResetSegmentTimer();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -67,33 +64,11 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
_log("FINAL: (empty)");
|
||||
}
|
||||
|
||||
CancelSegmentTimer();
|
||||
TransitionToPlaying();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSegmentTimer()
|
||||
{
|
||||
_segmentTimer?.Dispose();
|
||||
_segmentTimer = new System.Threading.Timer(_ => OnSegmentTimeout(), null, SegmentTimeout, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
private void CancelSegmentTimer()
|
||||
{
|
||||
_segmentTimer?.Dispose();
|
||||
_segmentTimer = null;
|
||||
}
|
||||
|
||||
private void OnSegmentTimeout()
|
||||
{
|
||||
_log("STT: segment timeout, starting playback early");
|
||||
lock (_stateLock)
|
||||
{
|
||||
TransitionToPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
private void TransitionToPlaying()
|
||||
{
|
||||
if (_playing)
|
||||
@@ -104,13 +79,11 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
if (_synthTask is null || _synthTask.IsCompleted)
|
||||
{
|
||||
_log("TTS: nothing to play");
|
||||
CancelSegmentTimer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_playing = true;
|
||||
CancelSegmentTimer();
|
||||
|
||||
int sampleRate = _bufferSampleRate;
|
||||
var chunks = _audioBuffer.ToList();
|
||||
@@ -189,7 +162,6 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
CancelSegmentTimer();
|
||||
_synthCts?.Cancel();
|
||||
_synthCts?.Dispose();
|
||||
_synthTask = null;
|
||||
@@ -209,7 +181,7 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
public async Task InitializeTtsAsync()
|
||||
{
|
||||
_log("Initializing TTS engine...");
|
||||
await _tts.InitializeAsync();
|
||||
await Task.Run(() => _tts.InitializeAsync());
|
||||
_log($"TTS ready (sample rate: {_tts.SampleRate} Hz)");
|
||||
}
|
||||
|
||||
@@ -269,15 +241,17 @@ internal sealed class Orchestrator : IAsyncDisposable
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_currentCts?.Cancel();
|
||||
_disposed = true;
|
||||
|
||||
try { _currentCts?.Cancel(); } catch { }
|
||||
_currentCts?.Dispose();
|
||||
_synthCts?.Cancel();
|
||||
|
||||
try { _synthCts?.Cancel(); } catch { }
|
||||
_synthCts?.Dispose();
|
||||
CancelSegmentTimer();
|
||||
|
||||
_sttSource.TranscriptReceived -= OnTranscript;
|
||||
await _sttSource.DisposeAsync();
|
||||
await _tts.DisposeAsync();
|
||||
_audioOutput.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Robovoice.Core\Robovoice.Core.csproj" />
|
||||
<ProjectReference Include="..\Robovoice.Tts.LibPiper\Robovoice.Tts.LibPiper.csproj" />
|
||||
<ProjectReference Include="..\Robovoice.Stt.Dhcp\Robovoice.Stt.Dhcp.csproj" />
|
||||
<ProjectReference Include="..\Robovoice.Stt.Tcp\Robovoice.Stt.Tcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user