clean code
This commit is contained in:
parent
7cbce42119
commit
84ae837173
|
|
@ -26,7 +26,7 @@ public class WaveStreamChannel : IStreamChannel
|
|||
{
|
||||
DeviceNumber = 0, // Default recording device
|
||||
WaveFormat = new WaveFormat(24000, 16, 1), // 24000 Hz, 16-bit PCM, Mono
|
||||
//BufferMilliseconds = 100
|
||||
BufferMilliseconds = 100
|
||||
};
|
||||
|
||||
// Set up the DataAvailable event handler
|
||||
|
|
@ -39,8 +39,7 @@ public class WaveStreamChannel : IStreamChannel
|
|||
var waveFormat = new WaveFormat(24000, 16, 1); // 24000 Hz, 16-bit PCM, Mono
|
||||
_bufferedWaveProvider = new BufferedWaveProvider(waveFormat);
|
||||
_bufferedWaveProvider.BufferDuration = TimeSpan.FromMinutes(10);
|
||||
//_bufferedWaveProvider.BufferLength = 1024 * 32; // Buffer length
|
||||
_bufferedWaveProvider.DiscardOnBufferOverflow = false;
|
||||
_bufferedWaveProvider.DiscardOnBufferOverflow = true;
|
||||
|
||||
_waveOut = new WaveOutEvent()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using BotSharp.Plugin.OpenAI.Models.Realtime;
|
||||
using BotSharp.Plugin.OpenAI.Providers.Realtime.Session;
|
||||
using OpenAI.Chat;
|
||||
using OpenAI.RealtimeConversation;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace BotSharp.Plugin.OpenAI.Providers.Realtime;
|
||||
|
||||
|
|
@ -20,7 +18,6 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
private readonly BotSharpOptions _options;
|
||||
|
||||
protected string _model = "gpt-4o-mini-realtime-preview";
|
||||
//private ClientWebSocket _webSocket;
|
||||
private RealtimeChatSession _session;
|
||||
|
||||
public RealTimeCompletionProvider(
|
||||
|
|
@ -50,7 +47,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
|
||||
_session?.Dispose();
|
||||
_session = new RealtimeChatSession(_services, _options);
|
||||
await _session.StartAsync(Provider, _model);
|
||||
await _session.ConnectAsync(Provider, _model, CancellationToken.None);
|
||||
|
||||
_ = ReceiveMessage(conn,
|
||||
onModelReady,
|
||||
|
|
@ -61,41 +58,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
onConversationItemCreated,
|
||||
onInputAudioTranscriptionCompleted,
|
||||
onInterruptionDetected);
|
||||
|
||||
|
||||
//var settingsService = _services.GetRequiredService<ILlmProviderService>();
|
||||
//var settings = settingsService.GetSetting(Provider, _model);
|
||||
|
||||
//_webSocket?.Dispose();
|
||||
//_webSocket = new ClientWebSocket();
|
||||
//_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {settings.ApiKey}");
|
||||
//_webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1");
|
||||
|
||||
//await _webSocket.ConnectAsync(new Uri($"wss://api.openai.com/v1/realtime?model={_model}"), CancellationToken.None);
|
||||
|
||||
//if (_webSocket.State == WebSocketState.Open)
|
||||
//{
|
||||
// // Receive a message
|
||||
// _ = ReceiveMessage(conn,
|
||||
// onModelReady,
|
||||
// onModelAudioDeltaReceived,
|
||||
// onModelAudioResponseDone,
|
||||
// onModelAudioTranscriptDone,
|
||||
// onModelResponseDone,
|
||||
// onConversationItemCreated,
|
||||
// onInputAudioTranscriptionCompleted,
|
||||
// onInterruptionDetected);
|
||||
//}
|
||||
}
|
||||
|
||||
public async Task Disconnect()
|
||||
{
|
||||
_session?.Disconnect();
|
||||
|
||||
//if (_webSocket.State == WebSocketState.Open)
|
||||
//{
|
||||
// await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
|
||||
//}
|
||||
}
|
||||
|
||||
public async Task AppenAudioBuffer(string message)
|
||||
|
|
@ -165,10 +132,9 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
Action<RoleDialogModel> onUserAudioTranscriptionCompleted,
|
||||
Action onInterruptionDetected)
|
||||
{
|
||||
await foreach (SessionConversationUpdate update in _session.ReceiveUpdatesAsync())
|
||||
await foreach (SessionConversationUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None))
|
||||
{
|
||||
var receivedText = update.RawResponse;
|
||||
//Console.WriteLine($"\r\n{receivedText?.Substring(0, 30)}\r\n");
|
||||
var receivedText = update?.RawResponse;
|
||||
if (string.IsNullOrEmpty(receivedText))
|
||||
{
|
||||
continue;
|
||||
|
|
@ -251,145 +217,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//private async Task ReceiveMessage(RealtimeHubConnection conn,
|
||||
// Action onModelReady,
|
||||
// Action<string,string> onModelAudioDeltaReceived,
|
||||
// Action onModelAudioResponseDone,
|
||||
// Action<string> onModelAudioTranscriptDone,
|
||||
// Action<List<RoleDialogModel>> onModelResponseDone,
|
||||
// Action<string> onConversationItemCreated,
|
||||
// Action<RoleDialogModel> onUserAudioTranscriptionCompleted,
|
||||
// Action onInterruptionDetected)
|
||||
//{
|
||||
// var buffer = new byte[1024 * 1024 * 32];
|
||||
// // Model response timeout
|
||||
// var settings = _services.GetRequiredService<RealtimeModelSettings>();
|
||||
// var timeout = settings.ModelResponseTimeout;
|
||||
// WebSocketReceiveResult? result = default;
|
||||
|
||||
// do
|
||||
// {
|
||||
// Array.Clear(buffer, 0, buffer.Length);
|
||||
|
||||
// var taskWorker = _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
// var taskTimer = Task.Delay(1000 * timeout);
|
||||
// var completedTask = await Task.WhenAny(taskWorker, taskTimer);
|
||||
|
||||
// if (completedTask == taskWorker)
|
||||
// {
|
||||
// result = taskWorker.Result;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// _logger.LogWarning($"Timeout {timeout} seconds waiting for Model response.");
|
||||
// await TriggerModelInference("Response user immediately");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // Convert received data to text/audio (Twilio sends Base64-encoded audio)
|
||||
// string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
// if (string.IsNullOrEmpty(receivedText))
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// var response = JsonSerializer.Deserialize<ServerEventResponse>(receivedText);
|
||||
|
||||
// _logger.LogDebug($"{nameof(RealTimeCompletionProvider)} received: {response.Type} {receivedText.Length}");
|
||||
|
||||
// if (response.Type == "error")
|
||||
// {
|
||||
// _logger.LogError($"{response.Type}: {receivedText}");
|
||||
// var error = JsonSerializer.Deserialize<ServerEventErrorResponse>(receivedText);
|
||||
// if (error?.Body.Type == "server_error")
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// else if (response.Type == "session.created")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// onModelReady();
|
||||
// }
|
||||
// else if (response.Type == "session.updated")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// }
|
||||
// else if (response.Type == "response.audio_transcript.delta")
|
||||
// {
|
||||
|
||||
// }
|
||||
// else if (response.Type == "response.audio_transcript.done")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// var data = JsonSerializer.Deserialize<ResponseAudioTranscript>(receivedText);
|
||||
// onModelAudioTranscriptDone(data.Transcript);
|
||||
// }
|
||||
// else if (response.Type == "response.audio.delta")
|
||||
// {
|
||||
// var audio = JsonSerializer.Deserialize<ResponseAudioDelta>(receivedText);
|
||||
// if (audio?.Delta != null)
|
||||
// {
|
||||
// _logger.LogDebug($"{response.Type}: {receivedText}");
|
||||
// onModelAudioDeltaReceived(audio.Delta, audio.ItemId);
|
||||
// }
|
||||
// }
|
||||
// else if (response.Type == "response.audio.done")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// onModelAudioResponseDone();
|
||||
// }
|
||||
// else if (response.Type == "response.done")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// var messages = await OnResponsedDone(conn, receivedText);
|
||||
// onModelResponseDone(messages);
|
||||
// }
|
||||
// else if (response.Type == "conversation.item.created")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// onConversationItemCreated(receivedText);
|
||||
// }
|
||||
// else if (response.Type == "conversation.item.input_audio_transcription.completed")
|
||||
// {
|
||||
// _logger.LogInformation($"{response.Type}: {receivedText}");
|
||||
// var message = await OnUserAudioTranscriptionCompleted(conn, receivedText);
|
||||
// if (!string.IsNullOrEmpty(message.Content))
|
||||
// {
|
||||
// onUserAudioTranscriptionCompleted(message);
|
||||
// }
|
||||
// }
|
||||
// else if (response.Type == "input_audio_buffer.speech_started")
|
||||
// {
|
||||
// // Handle user interuption
|
||||
// onInterruptionDetected();
|
||||
// }
|
||||
|
||||
// } while (!result.CloseStatus.HasValue);
|
||||
|
||||
// await _webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||
//}
|
||||
|
||||
public async Task SendEventToModel(object message)
|
||||
{
|
||||
if (_session == null) return;
|
||||
|
||||
await _session.SendEventToModel(message);
|
||||
|
||||
//if (_webSocket.State != WebSocketState.Open)
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
|
||||
//if (message is not string data)
|
||||
//{
|
||||
// data = JsonSerializer.Serialize(message, _options.JsonSerializerOptions);
|
||||
//}
|
||||
|
||||
//var buffer = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
//await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<string> UpdateSession(RealtimeHubConnection conn)
|
||||
|
|
@ -428,7 +260,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
Tools = functions,
|
||||
Modalities = [ "text", "audio" ],
|
||||
Temperature = Math.Max(options.Temperature ?? realtimeModelSettings.Temperature, 0.6f),
|
||||
MaxResponseOutputTokens = 4096,
|
||||
MaxResponseOutputTokens = realtimeModelSettings.MaxResponseOutputTokens,
|
||||
TurnDetection = new RealtimeSessionTurnDetection
|
||||
{
|
||||
InterruptResponse = false/*,
|
||||
|
|
@ -439,26 +271,22 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
InputAudioNoiseReduction = new InputAudioNoiseReduction
|
||||
{
|
||||
Type = "near_field"
|
||||
},
|
||||
InputAudioTranscription = new()
|
||||
{
|
||||
Model = "whisper-1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//if (realtimeModelSettings.InputAudioTranscribe)
|
||||
//{
|
||||
// var words = new List<string>();
|
||||
// HookEmitter.Emit<IRealtimeHook>(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)));
|
||||
if (realtimeModelSettings.InputAudioTranscribe)
|
||||
{
|
||||
var words = new List<string>();
|
||||
HookEmitter.Emit<IRealtimeHook>(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)));
|
||||
|
||||
// sessionUpdate.session.InputAudioTranscription = new InputAudioTranscription
|
||||
// {
|
||||
// Model = realtimeModelSettings.InputAudioTranscription.Model,
|
||||
// Language = realtimeModelSettings.InputAudioTranscription.Language,
|
||||
// Prompt = string.Join(", ", words.Select(x => x.ToLower().Trim()).Distinct()).SubstringMax(1024)
|
||||
// };
|
||||
//}
|
||||
sessionUpdate.session.InputAudioTranscription = new InputAudioTranscription
|
||||
{
|
||||
Model = realtimeModelSettings.InputAudioTranscription.Model,
|
||||
Language = realtimeModelSettings.InputAudioTranscription.Language,
|
||||
Prompt = string.Join(", ", words.Select(x => x.ToLower().Trim()).Distinct()).SubstringMax(1024)
|
||||
};
|
||||
}
|
||||
|
||||
await HookEmitter.Emit<IContentGeneratingHook>(_services, async hook =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using BotSharp.Plugin.OpenAI.Models.Realtime;
|
||||
using System.ClientModel;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
|
|
@ -7,10 +6,14 @@ namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session;
|
|||
public class AsyncWebsocketDataCollectionResult : AsyncCollectionResult<ClientResult>
|
||||
{
|
||||
private readonly WebSocket _webSocket;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
|
||||
public AsyncWebsocketDataCollectionResult(WebSocket webSocket)
|
||||
public AsyncWebsocketDataCollectionResult(
|
||||
WebSocket webSocket,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_webSocket = webSocket;
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public override ContinuationToken? GetContinuationToken(ClientResult page)
|
||||
|
|
@ -20,7 +23,7 @@ public class AsyncWebsocketDataCollectionResult : AsyncCollectionResult<ClientRe
|
|||
|
||||
public override async IAsyncEnumerable<ClientResult> GetRawPagesAsync()
|
||||
{
|
||||
await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket);
|
||||
await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket, _cancellationToken);
|
||||
while (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session;
|
|||
public class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator<ClientResult>
|
||||
{
|
||||
private readonly WebSocket _webSocket;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly byte[] _buffer;
|
||||
|
||||
public AsyncWebsocketDataResultEnumerator(
|
||||
WebSocket webSocket)
|
||||
WebSocket webSocket,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_webSocket = webSocket;
|
||||
_cancellationToken = cancellationToken;
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(1024 * 32);
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ public class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator<ClientResult>
|
|||
var response = new AiWebsocketPipelineResponse();
|
||||
while (!response.IsComplete)
|
||||
{
|
||||
var receivedResult = await _webSocket.ReceiveAsync(new(_buffer), CancellationToken.None);
|
||||
var receivedResult = await _webSocket.ReceiveAsync(new(_buffer), _cancellationToken);
|
||||
|
||||
if (receivedResult.CloseStatus.HasValue)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
using BotSharp.Plugin.OpenAI.Models.Realtime;
|
||||
using System;
|
||||
using System.ClientModel;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BotSharp.Plugin.OpenAI.Providers.Realtime.Session;
|
||||
|
||||
|
|
@ -14,7 +12,7 @@ public class RealtimeChatSession : IDisposable
|
|||
|
||||
private ClientWebSocket _webSocket;
|
||||
private readonly object _singleReceiveLock = new();
|
||||
private readonly SemaphoreSlim _clientSendSemaphore = new(initialCount: 1, maxCount: 1);
|
||||
private readonly SemaphoreSlim _clientEventSemaphore = new(initialCount: 1, maxCount: 1);
|
||||
private AsyncWebsocketDataCollectionResult _receivedCollectionResult;
|
||||
|
||||
public RealtimeChatSession(
|
||||
|
|
@ -25,7 +23,7 @@ public class RealtimeChatSession : IDisposable
|
|||
_options = options;
|
||||
}
|
||||
|
||||
public async Task StartAsync(string provider, string model)
|
||||
public async Task ConnectAsync(string provider, string model, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settingsService = _services.GetRequiredService<ILlmProviderService>();
|
||||
var settings = settingsService.GetSetting(provider, model);
|
||||
|
|
@ -35,10 +33,10 @@ public class RealtimeChatSession : IDisposable
|
|||
_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {settings.ApiKey}");
|
||||
_webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1");
|
||||
|
||||
await _webSocket.ConnectAsync(new Uri($"wss://api.openai.com/v1/realtime?model={model}"), CancellationToken.None);
|
||||
await _webSocket.ConnectAsync(new Uri($"wss://api.openai.com/v1/realtime?model={model}"), cancellationToken);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SessionConversationUpdate> ReceiveUpdatesAsync()
|
||||
public async IAsyncEnumerable<SessionConversationUpdate> ReceiveUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await foreach (ClientResult result in ReceiveInnerUpdatesAsync())
|
||||
{
|
||||
|
|
@ -47,11 +45,11 @@ public class RealtimeChatSession : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<ClientResult> ReceiveInnerUpdatesAsync()
|
||||
public async IAsyncEnumerable<ClientResult> ReceiveInnerUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_singleReceiveLock)
|
||||
{
|
||||
_receivedCollectionResult ??= new(_webSocket);
|
||||
_receivedCollectionResult ??= new(_webSocket, cancellationToken);
|
||||
}
|
||||
|
||||
await foreach (var result in _receivedCollectionResult)
|
||||
|
|
@ -78,7 +76,7 @@ public class RealtimeChatSession : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
await _clientSendSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
await _clientEventSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -92,7 +90,7 @@ public class RealtimeChatSession : IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
_clientSendSemaphore.Release();
|
||||
_clientEventSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
using BotSharp.Abstraction.MLTasks;
|
||||
using System.Buffers;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Threading;
|
||||
|
||||
namespace BotSharp.Test.RealtimeVoice;
|
||||
|
||||
public class LocalSession
|
||||
{
|
||||
private readonly IRealTimeCompletion _completion;
|
||||
|
||||
public LocalSession(
|
||||
IRealTimeCompletion completion)
|
||||
{
|
||||
_completion = completion;
|
||||
}
|
||||
|
||||
public async Task SendInputAudioAsync(Stream audio)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024 * 16);
|
||||
while (true)
|
||||
{
|
||||
int bytesRead = await audio.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ReadOnlyMemory<byte> audioMemory = buffer.AsMemory(0, bytesRead);
|
||||
BinaryData audioData = BinaryData.FromBytes(audioMemory);;
|
||||
await _completion.AppenAudioBuffer(audioData.ToArray(), audioData.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
using NAudio.Wave;
|
||||
|
||||
namespace BotSharp.Test.RealtimeVoice;
|
||||
|
||||
public class MicrophoneAudioStream : Stream, IDisposable
|
||||
{
|
||||
private const int SAMPLES_PER_SECOND = 24000;
|
||||
private const int BYTES_PER_SAMPLE = 2;
|
||||
private const int CHANNELS = 1;
|
||||
|
||||
// For simplicity, this is configured to use a static 10-second ring buffer.
|
||||
private readonly byte[] _buffer = new byte[BYTES_PER_SAMPLE * SAMPLES_PER_SECOND * CHANNELS * 10];
|
||||
private readonly object _bufferLock = new();
|
||||
private int _bufferReadPos = 0;
|
||||
private int _bufferWritePos = 0;
|
||||
|
||||
private readonly WaveInEvent _waveInEvent;
|
||||
|
||||
private MicrophoneAudioStream()
|
||||
{
|
||||
_waveInEvent = new()
|
||||
{
|
||||
WaveFormat = new WaveFormat(SAMPLES_PER_SECOND, BYTES_PER_SAMPLE * 8, CHANNELS),
|
||||
DeviceNumber = 0
|
||||
};
|
||||
_waveInEvent.DataAvailable += (_, e) =>
|
||||
{
|
||||
lock (_bufferLock)
|
||||
{
|
||||
int bytesToCopy = e.BytesRecorded;
|
||||
if (_bufferWritePos + bytesToCopy >= _buffer.Length)
|
||||
{
|
||||
int bytesToCopyBeforeWrap = _buffer.Length - _bufferWritePos;
|
||||
Array.Copy(e.Buffer, 0, _buffer, _bufferWritePos, bytesToCopyBeforeWrap);
|
||||
bytesToCopy -= bytesToCopyBeforeWrap;
|
||||
_bufferWritePos = 0;
|
||||
}
|
||||
Array.Copy(e.Buffer, e.BytesRecorded - bytesToCopy, _buffer, _bufferWritePos, bytesToCopy);
|
||||
_bufferWritePos += bytesToCopy;
|
||||
}
|
||||
};
|
||||
_waveInEvent.StartRecording();
|
||||
}
|
||||
|
||||
public static MicrophoneAudioStream Start() => new();
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => throw new NotImplementedException();
|
||||
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int totalCount = count;
|
||||
|
||||
int GetBytesAvailable() => _bufferWritePos < _bufferReadPos
|
||||
? _bufferWritePos + (_buffer.Length - _bufferReadPos)
|
||||
: _bufferWritePos - _bufferReadPos;
|
||||
|
||||
// For simplicity, we'll block until all requested data is available and not perform partial reads.
|
||||
while (GetBytesAvailable() < count)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
lock (_bufferLock)
|
||||
{
|
||||
if (_bufferReadPos + count >= _buffer.Length)
|
||||
{
|
||||
int bytesBeforeWrap = _buffer.Length - _bufferReadPos;
|
||||
Array.Copy(
|
||||
sourceArray: _buffer,
|
||||
sourceIndex: _bufferReadPos,
|
||||
destinationArray: buffer,
|
||||
destinationIndex: offset,
|
||||
length: bytesBeforeWrap);
|
||||
_bufferReadPos = 0;
|
||||
count -= bytesBeforeWrap;
|
||||
offset += bytesBeforeWrap;
|
||||
}
|
||||
|
||||
Array.Copy(_buffer, _bufferReadPos, buffer, offset, count);
|
||||
_bufferReadPos += count;
|
||||
}
|
||||
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_waveInEvent?.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ using BotSharp.Abstraction.Conversations;
|
|||
using BotSharp.OpenAPI;
|
||||
using System.Text.Json;
|
||||
using System.Reflection;
|
||||
using BotSharp.Test.RealtimeVoice;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
|
||||
var services = ServiceBuilder.CreateHostBuilder(Assembly.GetExecutingAssembly());
|
||||
var channel = services.GetRequiredService<IStreamChannel>();
|
||||
|
|
@ -23,7 +21,7 @@ var conv = new Conversation
|
|||
};
|
||||
conv = await convService.NewConversation(conv);
|
||||
|
||||
//await channel.ConnectAsync(conv.Id);
|
||||
await channel.ConnectAsync(conv.Id);
|
||||
|
||||
var hub = services.GetRequiredService<IRealtimeHub>();
|
||||
var conn = hub.SetHubConnection(conv.Id);
|
||||
|
|
@ -54,50 +52,36 @@ conn.OnModelUserInterrupted = () =>
|
|||
@event = "clear"
|
||||
});
|
||||
|
||||
var completer = services.GetServices<IRealTimeCompletion>().First(x => x.Provider == "openai");
|
||||
LocalSession session = new(completer);
|
||||
SpeakerOutput speakerOutput = new();
|
||||
|
||||
await hub.ConnectToModel(async data =>
|
||||
{
|
||||
var response = JsonSerializer.Deserialize<ModelResponseEvent>(data);
|
||||
if (response.Event == "clear")
|
||||
{
|
||||
//channel.ClearBuffer();
|
||||
Console.WriteLine("Before clearing audio buffer...");
|
||||
speakerOutput.ClearPlayback();
|
||||
channel.ClearBuffer();
|
||||
}
|
||||
else if (response.Event == "media")
|
||||
{
|
||||
var message = JsonSerializer.Deserialize<ModelResponseMediaEvent>(data);
|
||||
//await channel.SendAsync(Convert.FromBase64String(message.Media), CancellationToken.None);
|
||||
speakerOutput.EnqueueForPlayback(Convert.FromBase64String(message.Media));
|
||||
await channel.SendAsync(Convert.FromBase64String(message.Media), CancellationToken.None);
|
||||
}
|
||||
}, init: async data =>
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using MicrophoneAudioStream microphoneInput = MicrophoneAudioStream.Start();
|
||||
await session.SendInputAudioAsync(microphoneInput);
|
||||
});
|
||||
});
|
||||
|
||||
StreamReceiveResult result;
|
||||
var buffer = new byte[1024 * 8];
|
||||
|
||||
//do
|
||||
//{
|
||||
// var seg = new ArraySegment<byte>(buffer);
|
||||
// result = await channel.ReceiveAsync(seg, CancellationToken.None);
|
||||
do
|
||||
{
|
||||
var seg = new ArraySegment<byte>(buffer);
|
||||
result = await channel.ReceiveAsync(seg, CancellationToken.None);
|
||||
|
||||
// await hub.Completer.AppenAudioBuffer(seg, result.Count);
|
||||
await hub.Completer.AppenAudioBuffer(seg, result.Count);
|
||||
|
||||
// // Display the audio level
|
||||
// int audioLevel = CalculateAudioLevel(buffer, result.Count);
|
||||
// DisplayAudioLevel(audioLevel);
|
||||
//} while (result.Status == StreamChannelStatus.Open);
|
||||
// Display the audio level
|
||||
int audioLevel = CalculateAudioLevel(buffer, result.Count);
|
||||
DisplayAudioLevel(audioLevel);
|
||||
} while (result.Status == StreamChannelStatus.Open);
|
||||
|
||||
while (true) { }
|
||||
|
||||
int CalculateAudioLevel(byte[] buffer, int bytesRecorded)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
using NAudio.Wave;
|
||||
|
||||
namespace BotSharp.Test.RealtimeVoice;
|
||||
|
||||
public class SpeakerOutput : IDisposable
|
||||
{
|
||||
BufferedWaveProvider _waveProvider;
|
||||
WaveOutEvent _waveOutEvent;
|
||||
|
||||
public SpeakerOutput()
|
||||
{
|
||||
WaveFormat outputAudioFormat = new(
|
||||
rate: 24000,
|
||||
bits: 16,
|
||||
channels: 1);
|
||||
_waveProvider = new(outputAudioFormat)
|
||||
{
|
||||
BufferDuration = TimeSpan.FromMinutes(5),
|
||||
DiscardOnBufferOverflow = false
|
||||
};
|
||||
_waveOutEvent = new();
|
||||
_waveOutEvent.Init(_waveProvider);
|
||||
_waveOutEvent.Play();
|
||||
}
|
||||
|
||||
public void EnqueueForPlayback(byte[] audioData)
|
||||
{
|
||||
byte[] buffer = audioData?.ToArray() ?? [];
|
||||
_waveProvider.AddSamples(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public void ClearPlayback()
|
||||
{
|
||||
_waveProvider.ClearBuffer();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_waveOutEvent?.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,8 @@
|
|||
|
||||
"RealtimeModel": {
|
||||
"InputAudioFormat": "pcm16",
|
||||
"OutputAudioFormat": "pcm16"
|
||||
"OutputAudioFormat": "pcm16",
|
||||
"MaxResponseOutputTokens": 4096
|
||||
},
|
||||
|
||||
"PluginLoader": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue