v0.1: Robovoice — PTT re-voicing app
WinForms tray app that captures text (from file or direct input) and synthesizes speech via libpiper (Piper TTS), outputting to any audio device (VB-CABLE for virtual mic routing). Features: - Global PTT hotkey (configurable F1-F12) via WH_KEYBOARD_LL - Text file sequential reader (line-by-line on each PTT cycle) - Direct text input with Speak button - Voice manager: browse 147-voice Piper catalogue, download, remove - libpiper P/Invoke wrapper with UTF-8 marshaling, streaming chunks - BufferedWaveProvider streaming playback with trailing silence flush - Sentence terminator auto-append (fixes espeak-ng final-word drop) - Tray icon with minimize-to-tray Architecture: - Robovoice.Core: ITtsEngine, ISttSource interfaces, voice catalogue - Robovoice.Tts.LibPiper: P/Invoke wrapper for libpiper.dll - Robovoice.Stt.File: text file STT source (testing without mic server) - Robovoice.Stt.Udp: UDP client stub (for future Linux mic server) - Robovoice.App: WinForms UI, orchestrator, PTT hotkey, audio output
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using Robovoice.Core;
|
||||
using Robovoice.Stt.File;
|
||||
using Robovoice.Tts.LibPiper;
|
||||
|
||||
namespace Robovoice.App;
|
||||
|
||||
internal sealed class Orchestrator : IAsyncDisposable
|
||||
{
|
||||
private readonly LibPiperTtsEngine _tts;
|
||||
private readonly AudioOutput _audioOutput;
|
||||
private readonly FileSttSource _sttSource;
|
||||
private readonly Action<string> _log;
|
||||
private CancellationTokenSource? _currentCts;
|
||||
private bool _disposed;
|
||||
|
||||
public string OutputDeviceName { get; set; } = string.Empty;
|
||||
|
||||
public Orchestrator(
|
||||
LibPiperTtsEngine tts,
|
||||
AudioOutput audioOutput,
|
||||
FileSttSource sttSource,
|
||||
Action<string> log)
|
||||
{
|
||||
_tts = tts;
|
||||
_audioOutput = audioOutput;
|
||||
_audioOutput.Log = log;
|
||||
_sttSource = sttSource;
|
||||
_log = log;
|
||||
_sttSource.TranscriptReceived += OnTranscript;
|
||||
}
|
||||
|
||||
private void OnTranscript(object? sender, TranscriptEventArgs e)
|
||||
{
|
||||
if (e.Message.Type == TranscriptType.Final)
|
||||
{
|
||||
_ = SynthesizeAsync(e.Message.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitializeTtsAsync()
|
||||
{
|
||||
_log("Initializing TTS engine...");
|
||||
await _tts.InitializeAsync();
|
||||
_log($"TTS ready (sample rate: {_tts.SampleRate} Hz)");
|
||||
}
|
||||
|
||||
public async Task SynthesizeAsync(string text)
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
_currentCts = new CancellationTokenSource();
|
||||
var ct = _currentCts.Token;
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
_log($"FINAL: \"{text}\" ({text.Length} chars)");
|
||||
|
||||
try
|
||||
{
|
||||
bool started = false;
|
||||
int chunkCount = 0;
|
||||
int totalSamples = 0;
|
||||
|
||||
await foreach (var chunk in _tts.SynthesizeAsync(text, ct))
|
||||
{
|
||||
if (!started)
|
||||
{
|
||||
started = true;
|
||||
_audioOutput.Start(chunk.SampleRate, OutputDeviceName);
|
||||
_log($"TTS: first chunk ({sw.ElapsedMilliseconds}ms)");
|
||||
}
|
||||
|
||||
_audioOutput.WriteSamples(chunk.Samples);
|
||||
chunkCount++;
|
||||
totalSamples += chunk.Samples.Length;
|
||||
}
|
||||
|
||||
if (!started)
|
||||
{
|
||||
_log("TTS: no audio produced");
|
||||
}
|
||||
else
|
||||
{
|
||||
_audioOutput.Flush();
|
||||
double durationSec = (double)totalSamples / _tts.SampleRate;
|
||||
_log($"TTS: done ({chunkCount} chunks, {durationSec:F2}s audio, {sw.ElapsedMilliseconds}ms)");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_log("TTS: cancelled");
|
||||
_audioOutput.Stop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log($"TTS error: {ex.Message}");
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
}
|
||||
|
||||
public void TriggerNextLine()
|
||||
{
|
||||
_sttSource.EmitNext();
|
||||
}
|
||||
|
||||
public string? GetPendingLine() => _sttSource.GetPendingLine();
|
||||
public int GetCurrentLineIndex() => _sttSource.CurrentIndex;
|
||||
public int GetLineCount() => _sttSource.LineCount;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_currentCts?.Cancel();
|
||||
_currentCts?.Dispose();
|
||||
_sttSource.TranscriptReceived -= OnTranscript;
|
||||
await _sttSource.DisposeAsync();
|
||||
await _tts.DisposeAsync();
|
||||
_audioOutput.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user