refine speech storage

This commit is contained in:
Jicheng Lu 2024-08-28 15:34:41 -05:00
parent 4a870a3972
commit 8674ac9830
15 changed files with 125 additions and 44 deletions

View file

@ -24,7 +24,7 @@ public interface IFileStorageService
/// <param name="conversationId"></param>
/// <param name="messageIds"></param>
/// <returns></returns>
Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshots(string conversationId, IEnumerable<string> messageIds);
Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshotsAsync(string conversationId, IEnumerable<string> messageIds);
/// <summary>
/// Get the files that have been uploaded in the chat. No screenshot images are included.
@ -58,7 +58,7 @@ public interface IFileStorageService
#endregion
#region Speech
Task SaveSpeechFileAsync(string conversationId, string fileName, BinaryData data);
Task<BinaryData> RetrieveSpeechFileAsync(string conversationId, string fileName);
bool SaveSpeechFile(string conversationId, string fileName, BinaryData data);
BinaryData GetSpeechFile(string conversationId, string fileName);
#endregion
}

View file

@ -1,28 +1,38 @@
using System.IO;
namespace BotSharp.Core.Files.Services
namespace BotSharp.Core.Files.Services;
public partial class LocalFileStorageService
{
public partial class LocalFileStorageService
public bool SaveSpeechFile(string conversationId, string fileName, BinaryData data)
{
public async Task SaveSpeechFileAsync(string conversationId, string fileName, BinaryData data)
try
{
var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, TEXT_TO_SPEECH_FOLDER);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var filePath = Path.Combine(dir, fileName);
if (File.Exists(filePath)) return;
using var file = File.Create(filePath);
using var input = data.ToStream();
await input.CopyToAsync(file);
}
public async Task<BinaryData> RetrieveSpeechFileAsync(string conversationId, string fileName)
var filePath = Path.Combine(dir, fileName);
if (File.Exists(filePath)) return false;
using var fs = File.Create(filePath);
using var ds = data.ToStream();
ds.CopyTo(fs);
return true;
}
catch (Exception ex)
{
var path = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, TEXT_TO_SPEECH_FOLDER, fileName);
using var file = new FileStream(path, FileMode.Open, FileAccess.Read);
return await BinaryData.FromStreamAsync(file);
_logger.LogWarning($"Error when saving speech file. {fileName} ({conversationId})\r\n{ex.Message}\r\n{ex.InnerException}");
return false;
}
}
public BinaryData GetSpeechFile(string conversationId, string fileName)
{
var path = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, TEXT_TO_SPEECH_FOLDER, fileName);
using var file = new FileStream(path, FileMode.Open, FileAccess.Read);
return BinaryData.FromStream(file);
}
}

View file

@ -6,7 +6,7 @@ namespace BotSharp.Core.Files.Services;
public partial class LocalFileStorageService
{
public async Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshots(string conversationId, IEnumerable<string> messageIds)
public async Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshotsAsync(string conversationId, IEnumerable<string> messageIds)
{
var files = new List<MessageFileModel>();
if (string.IsNullOrEmpty(conversationId) || messageIds.IsNullOrEmpty())

View file

@ -10,12 +10,6 @@ public partial class LocalFileStorageService : IFileStorageService
private readonly ILogger<LocalFileStorageService> _logger;
private readonly string _baseDir;
private readonly IEnumerable<string> _audioTypes = new List<string>
{
"mp3",
"wav"
};
private const string CONVERSATION_FOLDER = "conversations";
private const string FILE_FOLDER = "files";
private const string USER_FILE_FOLDER = "user";

View file

@ -52,7 +52,7 @@ public class ReadPdfFn : IFunctionCallback
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var messageIds = dialogs.Select(x => x.MessageId).Distinct().ToList();
var screenshots = await fileStorage.GetMessageFileScreenshots(conversationId, messageIds);
var screenshots = await fileStorage.GetMessageFileScreenshotsAsync(conversationId, messageIds);
if (screenshots.IsNullOrEmpty()) return dialogs;

View file

@ -12,6 +12,7 @@ namespace BotSharp.Plugin.TencentCos.Modules
private readonly string _fullBucketName;
private readonly string _appId;
private readonly string _region;
public BucketClient(CosXmlServer cosXml, string fullBucketName, string appId, string region)
{
_cosXml = cosXml;
@ -141,7 +142,32 @@ namespace BotSharp.Plugin.TencentCos.Modules
var objects = info.contentsList;
return objects.Where(o => o.size > 0).Select(o => o.key).ToList();
}
catch (CosClientException clientEx)
{
throw new Exception(clientEx.Message);
}
catch (CosServerException serverEx)
{
throw new Exception(serverEx.Message);
}
}
public string? GetDirFile(string dir, string key)
{
try
{
var request = new GetBucketRequest(_fullBucketName);
request.SetPrefix($"{dir.TrimEnd('/')}/");
request.SetDelimiter("/");
var result = _cosXml.GetBucket(request);
var info = result.listBucket;
var objects = info.contentsList;
return objects.Where(o => o.size > 0).FirstOrDefault(o => o.key == key)?.key;
}
catch (CosClientException clientEx)
{

View file

@ -2,13 +2,38 @@ namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public Task SaveSpeechFileAsync(string conversationId, string fileName, BinaryData data)
public bool SaveSpeechFile(string conversationId, string fileName, BinaryData data)
{
throw new NotImplementedException();
try
{
var file = $"{CONVERSATION_FOLDER}/{conversationId}/{TEXT_TO_SPEECH_FOLDER}/{fileName}";
var exist = _cosClient.BucketClient.DoesObjectExist(file);
if (exist)
{
return false;
}
return _cosClient.BucketClient.UploadBytes(file, data.ToArray());
}
catch (Exception ex)
{
_logger.LogWarning($"Error when saving speech file. {fileName} ({conversationId})\r\n{ex.Message}\r\n{ex.InnerException}");
return false;
}
}
public Task<BinaryData> RetrieveSpeechFileAsync(string conversationId, string fileName)
public BinaryData GetSpeechFile(string conversationId, string fileName)
{
throw new NotImplementedException();
var dir = $"{CONVERSATION_FOLDER}/{conversationId}/{TEXT_TO_SPEECH_FOLDER}";
var key = $"{dir}/{fileName}";
var file = _cosClient.BucketClient.GetDirFile(dir, key);
if (string.IsNullOrWhiteSpace(file))
{
return BinaryData.Empty;
}
var bytes = _cosClient.BucketClient.DownloadFileBytes(file);
return BinaryData.FromBytes(bytes);
}
}

View file

@ -8,7 +8,7 @@ namespace BotSharp.Plugin.TencentCos.Services;
public partial class TencentCosService
{
public async Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshots(string conversationId, IEnumerable<string> messageIds)
public async Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshotsAsync(string conversationId, IEnumerable<string> messageIds)
{
var files = new List<MessageFileModel>();
if (string.IsNullOrEmpty(conversationId) || messageIds.IsNullOrEmpty())

View file

@ -27,7 +27,7 @@ public partial class TencentCosService : IFileStorageService
private const string USERS_FOLDER = "users";
private const string USER_AVATAR_FOLDER = "avatar";
private const string SESSION_FOLDER = "sessions";
private const string TEXT_TO_SPEECH_FOLDER = "speeches";
public TencentCosService(
TencentCosSettings settings,

View file

@ -31,7 +31,11 @@ public class TwilioVoiceController : TwilioController
[HttpPost("twilio/voice/welcome")]
public TwiMLResult InitiateConversation(VoiceRequest request, [FromQuery] string states)
{
if (request?.CallSid == null) throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
if (request?.CallSid == null)
{
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
}
string conversationId = $"TwilioVoice_{request.CallSid}";
var twilio = _services.GetRequiredService<TwilioService>();
var url = $"twilio/voice/{conversationId}/receive/0?states={states}";
@ -46,13 +50,16 @@ public class TwilioVoiceController : TwilioController
var twilio = _services.GetRequiredService<TwilioService>();
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum);
string text = (request.SpeechResult + "\r\n" + request.Digits).Trim();
if (!string.IsNullOrWhiteSpace(text))
{
messages.Add(text);
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, text);
}
VoiceResponse response;
if (messages.Count == 0 && seqNum == 0)
{
@ -64,6 +71,7 @@ public class TwilioVoiceController : TwilioController
{
messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum - 1);
}
var messageContent = string.Join("\r\n", messages);
var callerMessage = new CallerMessage()
{
@ -72,6 +80,7 @@ public class TwilioVoiceController : TwilioController
Content = messageContent,
From = request.From
};
if (!string.IsNullOrEmpty(states))
{
var kvp = states.Split(':');
@ -80,11 +89,12 @@ public class TwilioVoiceController : TwilioController
callerMessage.States.Add(kvp[0], kvp[1]);
}
}
await messageQueue.EnqueueAsync(callerMessage);
await messageQueue.EnqueueAsync(callerMessage);
int audioIndex = Random.Shared.Next(1, 5);
response = twilio.ReturnInstructions($"twilio/hold-on-{audioIndex}.mp3", $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 1);
}
return TwiML(response);
}
@ -95,10 +105,12 @@ public class TwilioVoiceController : TwilioController
var nextSeqNum = seqNum + 1;
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
var twilio = _services.GetRequiredService<TwilioService>();
if (request.SpeechResult != null)
{
await sessionManager.StageCallerMessageAsync(conversationId, nextSeqNum, request.SpeechResult);
}
var reply = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum);
VoiceResponse response;
if (reply == null)
@ -117,7 +129,7 @@ public class TwilioVoiceController : TwilioController
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var data = await completion.GenerateAudioFromTextAsync(indication);
var fileName = $"indication_{seqNum}.mp3";
await fileStorage.SaveSpeechFileAsync(conversationId, fileName, data);
fileStorage.SaveSpeechFile(conversationId, fileName, data);
speechPath = $"twilio/voice/speeches/{conversationId}/{fileName}";
}
response = twilio.ReturnInstructions(speechPath, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 2);
@ -137,17 +149,17 @@ public class TwilioVoiceController : TwilioController
{
response = twilio.ReturnInstructions($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}", $"twilio/voice/{conversationId}/receive/{nextSeqNum}?states={states}", true);
}
}
return TwiML(response);
}
[ValidateRequest]
[HttpGet("twilio/voice/speeches/{conversationId}/{fileName}")]
public async Task<FileContentResult> RetrieveSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName)
public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName)
{
var fileService = _services.GetRequiredService<IFileStorageService>();
var data = await fileService.RetrieveSpeechFileAsync(conversationId, fileName);
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var data = fileStorage.GetSpeechFile(conversationId, fileName);
var result = new FileContentResult(data.ToArray(), "audio/mpeg");
result.FileDownloadName = fileName;
return result;

View file

@ -7,6 +7,7 @@ namespace BotSharp.Plugin.Twilio.Services
{
private readonly Channel<CallerMessage> _queue;
internal ChannelReader<CallerMessage> Reader => _queue.Reader;
public TwilioMessageQueue()
{
BoundedChannelOptions options = new(100)
@ -18,7 +19,11 @@ namespace BotSharp.Plugin.Twilio.Services
public async ValueTask EnqueueAsync(CallerMessage request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
Console.WriteLine($"[{DateTime.UtcNow}] Enqueue {request}");
await _queue.Writer.WriteAsync(request);
}

View file

@ -57,22 +57,26 @@ namespace BotSharp.Plugin.Twilio.Services
{
using var scope = _serviceProvider.CreateScope();
var sp = scope.ServiceProvider;
AssistantMessage reply = null;
var inputMsg = new RoleDialogModel(AgentRole.User, message.Content);
var conv = sp.GetRequiredService<IConversationService>();
var routing = sp.GetRequiredService<IRoutingService>();
var config = sp.GetRequiredService<TwilioSetting>();
routing.Context.SetMessageId(message.ConversationId, inputMsg.MessageId);
var states = new List<MessageState>
{
new MessageState("channel", ConversationChannel.Phone),
new MessageState("calling_phone", message.From)
};
foreach (var kvp in message.States)
{
states.Add(new MessageState(kvp.Key, kvp.Value));
}
conv.SetConversationId(message.ConversationId, states);
var sessionManager = sp.GetRequiredService<ITwilioSessionManager>();
var result = await conv.SendMessage(config.AgentId,
inputMsg,
@ -93,14 +97,15 @@ namespace BotSharp.Plugin.Twilio.Services
await sessionManager.SetReplyIndicationAsync(message.ConversationId, message.SeqNumber, msg.Indication);
}
},
async functionExecuted =>
{ }
async functionExecuted => { }
);
var completion = CompletionProvider.GetAudioCompletion(sp, "openai", "tts-1");
var fileStorage = sp.GetRequiredService<IFileStorageService>();
var data = await completion.GenerateAudioFromTextAsync(reply.Content);
var fileName = $"reply_{reply.MessageId}.mp3";
await fileStorage.SaveSpeechFileAsync(message.ConversationId, fileName, data);
fileStorage.SaveSpeechFile(message.ConversationId, fileName, data);
reply.SpeechFileName = fileName;
reply.Content = null;
await sessionManager.SetAssistantReplyAsync(message.ConversationId, message.SeqNumber, reply);

View file

@ -59,6 +59,7 @@ public class TwilioService
},
Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{twilioSetting.AgentId}")
};
gather.Say(message);
response.Append(gather);
return response;
@ -80,6 +81,7 @@ public class TwilioService
Timeout = timeout > 0 ? timeout : 3,
ActionOnEmptyResult = actionOnEmptyResult
};
if (!string.IsNullOrEmpty(speechPath))
{
gather.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
@ -119,6 +121,7 @@ public class TwilioService
Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{twilioSetting.AgentId}"),
ActionOnEmptyResult = true
};
if (!string.IsNullOrEmpty(message))
{
gather.Say(message);

View file

@ -25,9 +25,7 @@ namespace BotSharp.Plugin.Twilio.Services
{
var db = _redis.GetDatabase();
var key = $"{conversationId}:Caller:{seqNum}";
return (await db.ListRangeAsync(key))
.Select(x => (string)x)
.ToList();
return (await db.ListRangeAsync(key)).Select(x => (string)x).ToList();
}
public async Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message)

View file

@ -17,9 +17,12 @@ public class TwilioPlugin : IBotSharpPlugin
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<TwilioSetting>("Twilio");
});
services.AddScoped<TwilioService>();
var conn = ConnectionMultiplexer.Connect(config["Twilio:RedisConnectionString"]);
var sessionManager = new TwilioSessionManager(conn);
services.AddSingleton<ITwilioSessionManager>(sessionManager);
services.AddSingleton<TwilioMessageQueue>();
services.AddHostedService<TwilioMessageQueueService>();