v0.8: TCP transport via gatuna tunnel, session IDs, pre-synth playback, server Rust rewrite

This commit is contained in:
2026-08-13 09:58:33 +00:00
parent ecf00c1bc4
commit facbfe6a5c
16 changed files with 955 additions and 638 deletions
+69 -101
View File
@@ -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();
}