refine api

This commit is contained in:
Jicheng Lu 2024-08-28 11:08:12 -05:00
parent 1ace4e44ab
commit 35eb7dabd6
49 changed files with 590 additions and 421 deletions

View file

@ -111,7 +111,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.PythonInter
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Graph", "Graph", "{97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Graph", "src\Plugins\BotSharp.Plugin.Graph\BotSharp.Plugin.Graph.csproj", "{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Graph", "src\Plugins\BotSharp.Plugin.Graph\BotSharp.Plugin.Graph.csproj", "{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.AudioHandler", "src\Plugins\BotSharp.Plugin.AudioHandler\BotSharp.Plugin.AudioHandler.csproj", "{F57F4862-F8D4-44A1-AC12-5C131B5C9785}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -449,6 +451,14 @@ Global
{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|Any CPU.Build.0 = Release|Any CPU
{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x64.ActiveCfg = Release|Any CPU
{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x64.Build.0 = Release|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x64.ActiveCfg = Debug|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x64.Build.0 = Debug|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.Build.0 = Release|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.ActiveCfg = Release|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -503,6 +513,7 @@ Global
{05E6E405-5021-406E-8A5E-0A7CEC881F6D} = {C4C59872-3C8A-450D-83D5-2BE402D610D5}
{97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D} = {97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1}
{F57F4862-F8D4-44A1-AC12-5C131B5C9785} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -21,7 +21,7 @@ public interface IFileInstructService
#endregion
#region Audio
Task<string> ReadAudio(string? provider, string? model, InstructFileModel audio);
Task<string> SpeechToText(string? provider, string? model, InstructFileModel audio, string? text = null);
#endregion
#region Select file

View file

@ -6,6 +6,7 @@ public interface IFileStorageService
{
#region Common
string GetDirectory(string conversationId);
IEnumerable<string> GetFiles(string relativePath, string? searchQuery = null);
byte[] GetFileBytes(string fileStorageUrl);
bool SaveFileStreamToPath(string filePath, Stream stream);
bool SaveFileBytesToPath(string filePath, byte[] bytes);

View file

@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Files.Models;
public class BotSharpFile : FileInfo
{
/// <summary>
/// File data, e.g., "data:image/png;base64,aaaaaaaa"
/// File data => format: "data:image/png;base64,aaaaaaaa"
/// </summary>
[JsonPropertyName("file_data")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]

View file

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using System.IO;
namespace BotSharp.Abstraction.Files.Utilities;
@ -26,6 +28,25 @@ public static class FileUtility
return (contentType, Convert.FromBase64String(base64Str));
}
public static string BuildFileDataFromFile(string fileName, byte[] bytes)
{
var contentType = GetFileContentType(fileName);
var base64 = Convert.ToBase64String(bytes);
return $"data:{contentType};base64,{base64}";
}
public static string BuildFileDataFromFile(IFormFile file)
{
using var stream = new MemoryStream();
file.CopyTo(stream);
stream.Position = 0;
var contentType = GetFileContentType(file.FileName);
var base64 = Convert.ToBase64String(stream.ToArray());
stream.Close();
return $"data:{contentType};base64,{base64}";
}
public static string GetFileContentType(string filePath)
{
string contentType;

View file

@ -0,0 +1,13 @@
using System.IO;
namespace BotSharp.Abstraction.MLTasks;
public interface IAudioCompletion
{
string Provider { get; }
Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName, string? text = null);
Task<BinaryData> GenerateSpeechFromTextAsync(string text);
void SetModelName(string model);
}

View file

@ -1,12 +0,0 @@
using System.IO;
namespace BotSharp.Abstraction.MLTasks;
public interface ISpeechToText
{
string Provider { get; }
Task<string> GenerateTextFromAudioAsync(string filePath);
Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName);
Task SetModelName(string model);
}

View file

@ -1,23 +0,0 @@
namespace BotSharp.Abstraction.MLTasks
{
public interface ITextToSpeech
{
/// <summary>
/// The LLM provider like Microsoft Azure, OpenAI, ClaudAI
/// </summary>
string Provider { get; }
/// <summary>
/// Set model name, one provider can consume different model or version(s)
/// </summary>
/// <param name="model">deployment name</param>
void SetModelName(string model);
Task<BinaryData> GenerateSpeechFromTextAsync(string text, ITextToSpeechOptions? options = null);
}
public interface ITextToSpeechOptions
{
}
}

View file

@ -4,16 +4,16 @@ namespace BotSharp.Core.Files.Services;
public partial class FileInstructService
{
public async Task<string> ReadAudio(string? provider, string? model, InstructFileModel audio)
public async Task<string> SpeechToText(string? provider, string? model, InstructFileModel audio, string? text = null)
{
var completion = CompletionProvider.GetSpeechToText(_services, provider: provider ?? "openai", model: model ?? "whisper-1");
var completion = CompletionProvider.GetAudioCompletion(_services, provider: provider ?? "openai", model: model ?? "whisper-1");
var audioBytes = await DownloadFile(audio);
using var stream = new MemoryStream();
stream.Write(audioBytes, 0, audioBytes.Length);
stream.Position = 0;
var fileName = $"{audio.FileName ?? "audio"}.{audio.FileExtension ?? "wav"}";
var content = await completion.GenerateTextFromAudioAsync(stream, fileName);
var content = await completion.GenerateTextFromAudioAsync(stream, fileName, text);
stream.Close();
return content;
}

View file

@ -14,6 +14,22 @@ public partial class LocalFileStorageService
return dir;
}
public IEnumerable<string> GetFiles(string relativePath, string? searchPattern = null)
{
if (string.IsNullOrWhiteSpace(relativePath))
{
return Enumerable.Empty<string>();
}
var path = Path.Combine(_baseDir, relativePath);
if (!string.IsNullOrWhiteSpace(searchPattern))
{
return Directory.GetFiles(path, searchPattern);
}
return Directory.GetFiles(path);
}
public byte[] GetFileBytes(string fileStorageUrl)
{
using var stream = File.OpenRead(fileStorageUrl);

View file

@ -28,6 +28,10 @@ public class CompletionProvider
{
return GetImageCompletion(services, provider: provider, model: model);
}
else if (settings.Type == LlmModelType.Audio)
{
return GetAudioCompletion(services, provider: provider, model: model);
}
else
{
return GetChatCompletion(services, provider: provider, model: model, agentConfig: agentConfig);
@ -108,7 +112,7 @@ public class CompletionProvider
if (completer == null)
{
var logger = services.GetRequiredService<ILogger<CompletionProvider>>();
logger.LogError($"Can't resolve completion provider by {provider}");
logger.LogError($"Can't resolve text-embedding provider by {provider}");
}
@ -120,35 +124,19 @@ public class CompletionProvider
return completer;
}
public static ITextToSpeech GetTextToSpeech(
public static IAudioCompletion GetAudioCompletion(
IServiceProvider services,
string provider,
string model)
{
var completions = services.GetServices<ITextToSpeech>();
var completions = services.GetServices<IAudioCompletion>();
var completer = completions.FirstOrDefault(x => x.Provider == provider);
if (completer == null)
{
var logger = services.GetRequiredService<ILogger<CompletionProvider>>();
logger.LogError($"Can't resolve text2speech provider by {provider}");
}
completer.SetModelName(model);
return completer;
logger.LogError($"Can't resolve audio-completion provider by {provider}");
}
public static ISpeechToText GetSpeechToText(
IServiceProvider services,
string provider,
string model
)
{
var completions = services.GetServices<ISpeechToText>();
var completer = completions.FirstOrDefault(x => x.Provider == provider);
if (completer == null)
{
var logger = services.GetRequiredService<ILogger<CompletionProvider>>();
logger.LogError($"Can't resolve speech2text provider by {provider}");
}
completer.SetModelName(model);
return completer;
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Instructs;
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Core.Infrastructures;
@ -91,11 +92,40 @@ public class InstructModeController : ControllerBase
}
catch (Exception ex)
{
var error = $"Error in analyzing files. {ex.Message}";
var error = $"Error in reading images. {ex.Message}";
_logger.LogError(error);
return error;
}
}
[HttpPost("/instruct/multi-modal/upload")]
public async Task<MultiModalViewModel> MultiModalCompletion(IFormFile file, [FromForm] string text, [FromForm] string? provider = null,
[FromForm] string? model = null, [FromForm] List<MessageState>? states = null)
{
var state = _services.GetRequiredService<IConversationStateService>();
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var viewModel = new MultiModalViewModel();
try
{
var data = FileUtility.BuildFileDataFromFile(file);
var files = new List<InstructFileModel>
{
new InstructFileModel { FileData = data }
};
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var content = await fileInstruct.ReadImages(provider, model, text, files);
viewModel.Content = content;
return viewModel;
}
catch (Exception ex)
{
var error = $"Error in reading image upload. {ex.Message}";
_logger.LogError(error);
viewModel.Message = error;
return viewModel;
}
}
#endregion
#region Generate image
@ -154,6 +184,38 @@ public class InstructModeController : ControllerBase
}
}
[HttpPost("/instruct/image-variation/upload")]
public async Task<ImageGenerationViewModel> ImageVariation(IFormFile file, [FromForm] string? provider = null,
[FromForm] string? model = null, [FromForm] List<MessageState>? states = null)
{
var state = _services.GetRequiredService<IConversationStateService>();
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var imageViewModel = new ImageGenerationViewModel();
try
{
var data = FileUtility.BuildFileDataFromFile(file);
var image = new InstructFileModel
{
FileName = Path.GetFileNameWithoutExtension(file.FileName),
FileExtension = Path.GetExtension(file.FileName).Substring(1),
FileData = data
};
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var message = await fileInstruct.VaryImage(provider, model, image);
imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages.Select(x => ImageViewModel.ToViewModel(x)).ToList();
return imageViewModel;
}
catch (Exception ex)
{
var error = $"Error in image variation upload. {ex.Message}";
_logger.LogError(error);
imageViewModel.Message = error;
return imageViewModel;
}
}
[HttpPost("/instruct/image-edit")]
public async Task<ImageGenerationViewModel> ImageEdit([FromBody] ImageEditRequest input)
{
@ -182,6 +244,38 @@ public class InstructModeController : ControllerBase
}
}
[HttpPost("/instruct/image-edit/upload")]
public async Task<ImageGenerationViewModel> ImageEdit(IFormFile file, [FromForm] string text, [FromForm] string? provider = null,
[FromForm] string? model = null, [FromForm] List<MessageState>? states = null)
{
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>();
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var imageViewModel = new ImageGenerationViewModel();
try
{
var data = FileUtility.BuildFileDataFromFile(file);
var image = new InstructFileModel
{
FileName = Path.GetFileNameWithoutExtension(file.FileName),
FileExtension = Path.GetExtension(file.FileName).Substring(1),
FileData = data
};
var message = await fileInstruct.EditImage(provider, model, text, image);
imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages.Select(x => ImageViewModel.ToViewModel(x)).ToList();
return imageViewModel;
}
catch (Exception ex)
{
var error = $"Error in image edit upload. {ex.Message}";
_logger.LogError(error);
imageViewModel.Message = error;
return imageViewModel;
}
}
[HttpPost("/instruct/image-mask-edit")]
public async Task<ImageGenerationViewModel> ImageMaskEdit([FromBody] ImageMaskEditRequest input)
{
@ -211,6 +305,47 @@ public class InstructModeController : ControllerBase
return imageViewModel;
}
}
[HttpPost("/instruct/image-mask-edit/upload")]
public async Task<ImageGenerationViewModel> ImageMaskEdit(IFormFile image, IFormFile mask, [FromForm] string text, [FromForm] string? provider = null,
[FromForm] string? model = null, [FromForm] List<MessageState>? states = null)
{
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>();
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var imageViewModel = new ImageGenerationViewModel();
try
{
var imageData = FileUtility.BuildFileDataFromFile(image);
var imageFile = new InstructFileModel
{
FileName = Path.GetFileNameWithoutExtension(image.FileName),
FileExtension = Path.GetExtension(image.FileName).Substring(1),
FileData = imageData
};
var maskData = FileUtility.BuildFileDataFromFile(mask);
var maskFile = new InstructFileModel
{
FileName = Path.GetFileNameWithoutExtension(mask.FileName),
FileExtension = Path.GetExtension(mask.FileName).Substring(1),
FileData = maskData
};
var message = await fileInstruct.EditImage(provider, model, text, imageFile, maskFile);
imageViewModel.Content = message.Content;
imageViewModel.Images = message.GeneratedImages.Select(x => ImageViewModel.ToViewModel(x)).ToList();
return imageViewModel;
}
catch (Exception ex)
{
var error = $"Error in image mask edit upload. {ex.Message}";
_logger.LogError(error);
imageViewModel.Message = error;
return imageViewModel;
}
}
#endregion
#region Pdf
@ -236,31 +371,91 @@ public class InstructModeController : ControllerBase
return viewModel;
}
}
[HttpPost("/instruct/pdf-completion/upload")]
public async Task<PdfCompletionViewModel> PdfCompletion(IFormFile file, [FromForm] string text, [FromForm] string? provider = null,
[FromForm] string? model = null, [FromForm] string? modelId = null, [FromForm] List<MessageState>? states = null)
{
var state = _services.GetRequiredService<IConversationStateService>();
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var viewModel = new PdfCompletionViewModel();
try
{
var data = FileUtility.BuildFileDataFromFile(file);
var files = new List<InstructFileModel>
{
new InstructFileModel { FileData = data }
};
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var content = await fileInstruct.ReadPdf(provider, model, modelId, text, files);
viewModel.Content = content;
return viewModel;
}
catch (Exception ex)
{
var error = $"Error in pdf completion upload. {ex.Message}";
_logger.LogError(error);
viewModel.Message = error;
return viewModel;
}
}
#endregion
#region Audio
[HttpPost("/instruct/audio-completion")]
public async Task<AudioCompletionViewModel> AudioCompletion([FromBody] AudioCompletionRequest input)
[HttpPost("/instruct/speech-to-text")]
public async Task<SpeechToTextViewModel> SpeechToText([FromBody] SpeechToTextRequest input)
{
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>();
input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var viewModel = new AudioCompletionViewModel();
var viewModel = new SpeechToTextViewModel();
try
{
var audio = input.File;
if (audio == null)
{
return new AudioCompletionViewModel { Message = "Error! Cannot find a valid audio file!" };
return new SpeechToTextViewModel { Message = "Error! Cannot find a valid audio file!" };
}
var content = await fileInstruct.ReadAudio(input.Provider, input.Model, audio);
var content = await fileInstruct.SpeechToText(input.Provider, input.Model, audio);
viewModel.Content = content;
return viewModel;
}
catch (Exception ex)
{
var error = $"Error in audio completion. {ex.Message}";
var error = $"Error in speech to text. {ex.Message}";
_logger.LogError(error);
viewModel.Message = error;
return viewModel;
}
}
[HttpPost("/instruct/speech-to-text/upload")]
public async Task<SpeechToTextViewModel> SpeechToText(IFormFile file, [FromForm] string? provider = null, [FromForm] string? model = null,
[FromForm] string? text = null, [FromForm] List<MessageState>? states = null)
{
var fileInstruct = _services.GetRequiredService<IFileInstructService>();
var state = _services.GetRequiredService<IConversationStateService>();
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var viewModel = new SpeechToTextViewModel();
try
{
using var stream = new MemoryStream();
file.CopyTo(stream);
stream.Position = 0;
var completion = CompletionProvider.GetAudioCompletion(_services, provider: provider ?? "openai", model: model ?? "whisper-1");
var content = await completion.GenerateTextFromAudioAsync(stream, file.FileName, text);
viewModel.Content = content;
stream.Close();
return viewModel;
}
catch (Exception ex)
{
var error = $"Error in speech-to-text upload. {ex.Message}";
_logger.LogError(error);
viewModel.Message = error;
return viewModel;

View file

@ -1,5 +0,0 @@
namespace BotSharp.OpenAPI.ViewModels.Instructs;
public class AudioCompletionViewModel : InstructBaseViewModel
{
}

View file

@ -59,8 +59,11 @@ public class ImageMaskEditRequest : InstructBaseRequest
public InstructFileModel Mask { get; set; }
}
public class AudioCompletionRequest : InstructBaseRequest
public class SpeechToTextRequest : InstructBaseRequest
{
[JsonPropertyName("text")]
public string? Text { get; set; }
[JsonPropertyName("file")]
public InstructFileModel File { get; set; }
}

View file

@ -0,0 +1,5 @@
namespace BotSharp.OpenAPI.ViewModels.Instructs;
public class MultiModalViewModel : InstructBaseViewModel
{
}

View file

@ -0,0 +1,5 @@
namespace BotSharp.OpenAPI.ViewModels.Instructs;
public class SpeechToTextViewModel : InstructBaseViewModel
{
}

View file

@ -16,8 +16,7 @@ public class AudioHandlerPlugin : IBotSharpPlugin
return settingService.Bind<AudioHandlerSettings>("AudioHandler");
});
services.AddScoped<ISpeechToText, NativeWhisperProvider>();
services.AddScoped<IAudioHelper, AudioHelper>();
services.AddScoped<IAudioCompletion, NativeWhisperProvider>();
services.AddScoped<IAgentHook, AudioHandlerHook>();
services.AddScoped<IAgentUtilityHook, AudioHandlerUtilityHook>();
}

View file

@ -23,4 +23,18 @@
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\handle_audio_request.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\handle_audio_request.fn.liquid" />
</ItemGroup>
<ItemGroup>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\handle_audio_request.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\handle_audio_request.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -1,58 +0,0 @@
using System.Diagnostics;
using BotSharp.Core.Infrastructures;
namespace BotSharp.Plugin.AudioHandler.Controllers
{
#if DEBUG
[AllowAnonymous]
#endif
[ApiController]
public class AudioController : ControllerBase
{
private readonly ISpeechToText _nativeWhisperProvider;
private readonly IServiceProvider _services;
public AudioController(ISpeechToText nativeWhisperProvider, IServiceProvider service)
{
_nativeWhisperProvider = nativeWhisperProvider;
_services = service;
}
[HttpGet("audio/transcript")]
public async Task<IActionResult> GetTextFromAudioController(string audioInputString, string modelType = "")
{
#if DEBUG
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
#endif
await _nativeWhisperProvider.SetModelName(modelType);
var result = await _nativeWhisperProvider.GenerateTextFromAudioAsync(audioInputString);
#if DEBUG
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
Console.WriteLine("RunTime " + elapsedTime);
#endif
return Ok(result);
}
[HttpPost("openai/audio/transcript")]
public async Task<IActionResult> GetTextFromAudioOpenAiController(string filePath)
{
#if DEBUG
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
#endif
var client = CompletionProvider.GetSpeechToText(_services, "openai", "whisper-1");
var result = await client.GenerateTextFromAudioAsync(filePath);
#if DEBUG
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
Console.WriteLine("RunTime " + elapsedTime);
#endif
return Ok(result);
}
}
}

View file

@ -9,6 +9,7 @@ public class HandleAudioRequestFn : IFunctionCallback
public string Indication => "Handling audio request";
private readonly IServiceProvider _serviceProvider;
private readonly IFileStorageService _fileStorage;
private readonly ILogger<HandleAudioRequestFn> _logger;
private readonly BotSharpOptions _options;
@ -19,10 +20,12 @@ public class HandleAudioRequestFn : IFunctionCallback
};
public HandleAudioRequestFn(
IFileStorageService fileStorage,
IServiceProvider serviceProvider,
ILogger<HandleAudioRequestFn> logger,
BotSharpOptions options)
{
_fileStorage = fileStorage;
_serviceProvider = serviceProvider;
_logger = logger;
_options = options;
@ -43,11 +46,13 @@ public class HandleAudioRequestFn : IFunctionCallback
private List<RoleDialogModel> AssembleFiles(string convId, List<RoleDialogModel> dialogs)
{
if (dialogs.IsNullOrEmpty()) return new List<RoleDialogModel>();
if (dialogs.IsNullOrEmpty())
{
return new List<RoleDialogModel>();
}
var fileService = _serviceProvider.GetRequiredService<IFileStorageService>();
var messageId = dialogs.Select(x => x.MessageId).Distinct().ToList();
var audioMessageFiles = fileService.GetMessageFiles(convId, messageId, FileSourceType.User, _audioContentType);
var audioMessageFiles = _fileStorage.GetMessageFiles(convId, messageId, FileSourceType.User, _audioContentType);
audioMessageFiles = audioMessageFiles.Where(x => x.ContentType.Contains("audio")).ToList();
@ -69,53 +74,46 @@ public class HandleAudioRequestFn : IFunctionCallback
private async Task<string> GetResponeFromDialogs(List<RoleDialogModel> dialogs)
{
var speech2Text = await PrepareModel("native");
var audioCompletion = PrepareModel();
var dialog = dialogs.Where(x => !x.Files.IsNullOrEmpty()).Last();
int transcribedCount = 0;
var transcripts = new List<string>();
foreach (var file in dialog.Files)
{
if (file == null) continue;
if (file == null || string.IsNullOrWhiteSpace(file.FileStorageUrl)) continue;
string extension = Path.GetExtension(file?.FileStorageUrl);
if (ParseAudioFileType(extension) && File.Exists(file.FileStorageUrl))
{
file.FileData = await speech2Text.GenerateTextFromAudioAsync(file.FileStorageUrl);
transcribedCount++;
}
var extension = Path.GetExtension(file.FileStorageUrl);
var fileName = Path.GetFileName(file.FileStorageUrl);
if (!ParseAudioFileType(fileName)) continue;
var bytes = _fileStorage.GetFileBytes(file.FileStorageUrl);
using var stream = new MemoryStream(bytes);
stream.Position = 0;
var result = await audioCompletion.GenerateTextFromAudioAsync(stream, fileName);
transcripts.Add(result);
stream.Close();
}
if (transcribedCount == 0)
if (transcripts.IsNullOrEmpty())
{
throw new FileNotFoundException($"No audio files found in the dialog. MessageId: {dialog.MessageId}");
}
var resList = dialog.Files.Select(x => $"{x.FileName} \r\n {x.FileData}").ToList();
return string.Join("\n\r", resList);
return string.Join("\r\n\r\n", transcripts);
}
private async Task<ISpeechToText> PrepareModel(string provider = "native")
private IAudioCompletion PrepareModel()
{
var speech2Text = _serviceProvider.GetServices<ISpeechToText>().FirstOrDefault(x => x.Provider == provider.ToLower());
if (speech2Text == null)
{
throw new Exception($"Can't resolve speech2text provider by {provider}");
return CompletionProvider.GetAudioCompletion(_serviceProvider, provider: "openai", model: "whisper-1");
}
if (provider.IsEqualTo("openai"))
private bool ParseAudioFileType(string fileName)
{
return CompletionProvider.GetSpeechToText(_serviceProvider, provider: "openai", model: "whisper-1");
}
await speech2Text.SetModelName("Tiny");
return speech2Text;
}
private bool ParseAudioFileType(string fileType)
{
fileType = fileType.ToLower();
var extension = Path.GetExtension(fileName).TrimStart('.').ToLower();
var provider = new FileExtensionContentTypeProvider();
bool canParse = Enum.TryParse<AudioType>(fileType, out _) || provider.TryGetContentType(fileType, out _);
bool canParse = Enum.TryParse<AudioType>(extension, out _) || provider.TryGetContentType(fileName, out _);
return canParse;
}
}

View file

@ -3,78 +3,83 @@ using NAudio.Wave.SampleProviders;
namespace BotSharp.Plugin.AudioHandler.Helpers;
public class AudioHelper : IAudioHelper
public static class AudioHelper
{
private readonly IServiceProvider _services;
private readonly ILogger<AudioHelper> _logger;
private const int DEFAULT_SAMPLE_RATE = 16000;
public AudioHelper(
IServiceProvider services,
ILogger<AudioHelper> logger)
{
_services = services;
_logger = logger;
}
public Stream ConvertToStream(string fileName)
public static Stream ConvertToStream(string fileName)
{
if (string.IsNullOrEmpty(fileName))
{
var error = "fileName is Null when converting to stream in audio processor";
_logger.LogWarning(error);
throw new ArgumentNullException(error);
throw new ArgumentNullException("fileName is Null when converting to stream in audio processor");
}
var fileExtension = Path.GetExtension(fileName).ToLower().TrimStart('.');
if (!Enum.TryParse(fileExtension, out AudioType fileType))
{
var error = $"File extension: '{fileExtension}' is not supported!";
_logger.LogWarning(error);
throw new NotSupportedException(error);
throw new NotSupportedException($"File extension: '{fileExtension}' is not supported!");
}
var stream = fileType switch
{
AudioType.mp3 => ConvertMp3ToStream(fileName),
AudioType.wav => ConvertWavToStream(fileName),
_ => throw new NotSupportedException("File extension not supported"),
_ => ConvertWavToStream(fileName)
};
return stream;
}
private Stream ConvertMp3ToStream(string fileName)
public static Stream Transform(Stream stream, string fileName)
{
var fileStream = File.OpenRead(fileName);
var fileExtension = Path.GetExtension(fileName).ToLower().TrimStart('.');
if (!Enum.TryParse(fileExtension, out AudioType fileType))
{
throw new NotSupportedException($"File extension: '{fileExtension}' is not supported!");
}
Stream resultStream = new MemoryStream();
stream.CopyTo(resultStream);
resultStream.Seek(0, SeekOrigin.Begin);
WaveStream reader = fileType switch
{
AudioType.mp3 => new Mp3FileReader(resultStream),
_ => new WaveFileReader(resultStream)
};
resultStream = ChangeSampleRate(reader);
reader.Close();
return resultStream;
}
private static Stream ConvertMp3ToStream(string fileName)
{
using var fileStream = File.OpenRead(fileName);
using var reader = new Mp3FileReader(fileStream);
if (reader.WaveFormat.SampleRate != 16000)
{
var wavStream = new MemoryStream();
var resampler = new WdlResamplingSampleProvider(reader.ToSampleProvider(), 16000);
WaveFileWriter.WriteWavFileToStream(wavStream, resampler.ToWaveProvider16());
wavStream.Seek(0, SeekOrigin.Begin);
return wavStream;
return ChangeSampleRate(reader);
}
fileStream.Seek(0, SeekOrigin.Begin);
return fileStream;
}
private Stream ConvertWavToStream(string fileName)
private static Stream ConvertWavToStream(string fileName)
{
var fileStream = File.OpenRead(fileName);
using var fileStream = File.OpenRead(fileName);
using var reader = new WaveFileReader(fileStream);
if (reader.WaveFormat.SampleRate != 16000)
{
var wavStream = new MemoryStream();
var resampler = new WdlResamplingSampleProvider(reader.ToSampleProvider(), 16000);
WaveFileWriter.WriteWavFileToStream(wavStream, resampler.ToWaveProvider16());
wavStream.Seek(0, SeekOrigin.Begin);
return wavStream;
return ChangeSampleRate(reader);
}
fileStream.Seek(0, SeekOrigin.Begin);
return fileStream;
private static Stream ChangeSampleRate(WaveStream ws)
{
var ms = new MemoryStream();
if (ws.WaveFormat.SampleRate != DEFAULT_SAMPLE_RATE)
{
var resampler = new WdlResamplingSampleProvider(ws.ToSampleProvider(), DEFAULT_SAMPLE_RATE);
WaveFileWriter.WriteWavFileToStream(ms, resampler.ToWaveProvider16());
}
else
{
ws.CopyTo(ms);
}
ms.Seek(0, SeekOrigin.Begin);
return ms;
}
}

View file

@ -1,6 +0,0 @@
namespace BotSharp.Plugin.AudioHandler.Helpers;
public interface IAudioHelper
{
Stream ConvertToStream(string fileName);
}

View file

@ -1,6 +1,5 @@
using BotSharp.Abstraction.Agents.Settings;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
namespace BotSharp.Plugin.AudioHandler.Hooks;
@ -22,17 +21,16 @@ public class AudioHandlerHook : AgentHookBase, IAgentHook
if (isEnabled && isConvMode)
{
AddUtility(agent, UtilityName.AudioHandler, HANDLER_AUDIO);
AddUtility(agent, HANDLER_AUDIO);
}
base.OnAgentLoaded(agent);
}
private void AddUtility(Agent agent, string utility, string functionName)
private void AddUtility(Agent agent, string functionName)
{
if (!IsEnableUtility(agent, utility)) return;
var (prompt, fn) = GetPromptAndFunction(functionName);
if (fn != null)
{
if (!string.IsNullOrWhiteSpace(prompt))
@ -51,11 +49,6 @@ public class AudioHandlerHook : AgentHookBase, IAgentHook
}
}
private bool IsEnableUtility(Agent agent, string utility)
{
return !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(utility);
}
private (string, FunctionDef?) GetPromptAndFunction(string functionName)
{
var db = _services.GetRequiredService<IBotSharpRepository>();

View file

@ -6,49 +6,39 @@ namespace BotSharp.Plugin.AudioHandler.Provider;
/// <summary>
/// Native Whisper provider for speech to text conversion
/// </summary>
public class NativeWhisperProvider : ISpeechToText
public class NativeWhisperProvider : IAudioCompletion
{
private readonly IAudioHelper _audioProcessor;
private static WhisperProcessor _whisperProcessor;
private readonly IServiceProvider _services;
private readonly IFileStorageService _fileStorage;
private readonly ILogger<NativeWhisperProvider> _logger;
public string Provider => "native";
private string MODEL_DIR = "model";
private string? _currentModelPath;
private Dictionary<GgmlType, string> _modelPathDict = new Dictionary<GgmlType, string>();
private GgmlType? _modelType;
public NativeWhisperProvider(
IAudioHelper audioProcessor,
BotSharpDatabaseSettings dbSettings,
IFileStorageService fileStorage,
IServiceProvider services,
ILogger<NativeWhisperProvider> logger)
{
_audioProcessor = audioProcessor;
_fileStorage = fileStorage;
_services = services;
_logger = logger;
}
public async Task<string> GenerateTextFromAudioAsync(string filePath)
public async Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName, string? text = null)
{
string fileExtension = Path.GetExtension(filePath);
if (!Enum.TryParse(fileExtension.TrimStart('.').ToLower(), out AudioType audioType))
{
throw new Exception($"Unsupported audio type: {fileExtension}");
}
using var stream = _audioProcessor.ConvertToStream(filePath);
if (stream == null)
{
throw new Exception($"Failed to convert {fileExtension} to stream");
}
var textResult = new List<SegmentData>();
using var stream = AudioHelper.Transform(audio, audioFileName);
await foreach (var result in _whisperProcessor.ProcessAsync(stream).ConfigureAwait(false))
{
textResult.Add(result);
}
_whisperProcessor.Dispose();
stream.Close();
var audioOutput = new AudioOutput
{
@ -57,54 +47,45 @@ public class NativeWhisperProvider : ISpeechToText
return audioOutput.ToString();
}
public Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName)
public async Task<BinaryData> GenerateSpeechFromTextAsync(string text)
{
throw new NotImplementedException();
}
public async Task SetModelName(string model)
public void SetModelName(string model)
{
if (Enum.TryParse(model, true, out GgmlType ggmlType))
{
await LoadWhisperModel(ggmlType);
return;
LoadWhisperModel(ggmlType);
}
else
{
_logger.LogWarning($"Unsupported model type: {model}. Use Tiny model instead!");
await LoadWhisperModel(GgmlType.Tiny);
LoadWhisperModel(GgmlType.Tiny);
}
}
private async Task LoadWhisperModel(GgmlType modelType)
private void LoadWhisperModel(GgmlType modelType)
{
try
{
if (!Directory.Exists(MODEL_DIR))
var modelDir = _fileStorage.BuildDirectory("models", "whisper");
var exist = _fileStorage.ExistDirectory(modelDir);
if (!exist)
{
Directory.CreateDirectory(MODEL_DIR);
_fileStorage.CreateDirectory(modelDir);
}
var availableModelPaths = Directory.GetFiles(MODEL_DIR, "*.bin").ToArray();
if (availableModelPaths.IsNullOrEmpty())
var files = _fileStorage.GetFiles("models/whisper", "*.bin");
var modelLoc = files.FirstOrDefault(x => Path.GetFileName(x) == BuildModelFile(modelType));
if (string.IsNullOrEmpty(modelLoc))
{
_currentModelPath = SetModelPath(MODEL_DIR, modelType);
await DownloadModel(modelType, _currentModelPath);
}
else
{
var modelFilePath = availableModelPaths.FirstOrDefault(x => Path.GetFileName(x) == $"ggml-{modelType}.bin");
if (modelFilePath == null)
{
_currentModelPath = SetModelPath(MODEL_DIR, modelType);
await DownloadModel(modelType, _currentModelPath);
}
else
{
_currentModelPath = modelFilePath;
}
modelLoc = BuildModelPath(modelType);
DownloadModel(modelType, modelLoc);
}
_whisperProcessor = WhisperFactory.FromPath(path: _currentModelPath).CreateBuilder().WithLanguage("auto").Build();
_modelType = modelType;
var bytes = _fileStorage.GetFileBytes(modelLoc);
_whisperProcessor = WhisperFactory.FromBuffer(buffer: bytes).CreateBuilder().WithLanguage("auto").Build();
}
catch (Exception ex)
{
@ -114,16 +95,20 @@ public class NativeWhisperProvider : ISpeechToText
}
}
private async Task DownloadModel(GgmlType modelType, string modelDir)
private void DownloadModel(GgmlType modelType, string modelDir)
{
using var modelStream = await WhisperGgmlDownloader.GetGgmlModelAsync(modelType);
using var fileWriter = File.OpenWrite(modelDir);
await modelStream.CopyToAsync(fileWriter);
using var modelStream = WhisperGgmlDownloader.GetGgmlModelAsync(modelType).ConfigureAwait(false).GetAwaiter().GetResult();
_fileStorage.SaveFileStreamToPath(modelDir, modelStream);
modelStream.Close();
}
private string SetModelPath(string rootPath, GgmlType modelType)
private string BuildModelPath(GgmlType modelType)
{
string currentModelPath = Path.Combine(rootPath, $"ggml-{modelType}.bin");
return currentModelPath;
return _fileStorage.BuildDirectory("models", "whisper", BuildModelFile(modelType));
}
private string BuildModelFile(GgmlType modelType)
{
return $"ggml-{modelType}.bin";
}
}

View file

@ -5,6 +5,7 @@ global using System.Linq;
global using System.Text.Json;
global using System.Threading.Tasks;
global using BotSharp.Abstraction.Repositories;
global using BotSharp.Abstraction.Agents;
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Agents.Models;

View file

@ -84,11 +84,12 @@ public class HandleEmailSenderFn : IFunctionCallback
{
if (files.IsNullOrEmpty()) return;
var fileStorage = _services.GetRequiredService<IFileStorageService>();
foreach (var file in files)
{
if (string.IsNullOrEmpty(file.FileStorageUrl)) continue;
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var fileBytes = fileStorage.GetFileBytes(file.FileStorageUrl);
builder.Attachments.Add($"{file.FileName}.{file.FileExtension}", fileBytes, ContentType.Parse(file.ContentType));
Thread.Sleep(100);

View file

@ -1,14 +1,7 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Settings;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Plugin.EmailHandler.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BotSharp.Plugin.EmailHandler.Hooks;

View file

@ -1,14 +1,7 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Settings;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Plugin.EmailHandler.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BotSharp.Plugin.EmailHandler.Hooks;

View file

@ -35,7 +35,7 @@ public class GraphDb : IGraphDb
_settings = settings;
}
public string Name => "Neo4j";
public string Name => "Remote";
public async Task<GraphSearchData> Search(string query, GraphSearchOptions options)
{

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>

View file

@ -18,7 +18,7 @@ public class KnowledgeRetrievalFn : IFunctionCallback
var args = JsonSerializer.Deserialize<ExtractedKnowledge>(message.FunctionArgs ?? "{}");
var collectionName = _settings.Default.CollectionName ?? KnowledgeCollectionName.BotSharp;
var embedding = KnowledgeSettingUtility.GetTextEmbeddingSetting(_services, collectionName);
var embedding = KnowledgeSettingHelper.GetTextEmbeddingSetting(_services, collectionName);
var vector = await embedding.GetVectorAsync(args.Question);
var vectorDb = _services.GetServices<IVectorDb>().FirstOrDefault(x => x.Name == _settings.VectorDb);

View file

@ -18,7 +18,7 @@ public class MemorizeKnowledgeFn : IFunctionCallback
var args = JsonSerializer.Deserialize<ExtractedKnowledge>(message.FunctionArgs ?? "{}");
var collectionName = _settings.Default.CollectionName ?? KnowledgeCollectionName.BotSharp;
var embedding = KnowledgeSettingUtility.GetTextEmbeddingSetting(_services, collectionName);
var embedding = KnowledgeSettingHelper.GetTextEmbeddingSetting(_services, collectionName);
var vector = await embedding.GetVectorsAsync(new List<string>
{

View file

@ -1,6 +1,6 @@
namespace BotSharp.Plugin.KnowledgeBase.Utilities;
namespace BotSharp.Plugin.KnowledgeBase.Helpers;
public static class KnowledgeSettingUtility
public static class KnowledgeSettingHelper
{
public static ITextEmbedding GetTextEmbeddingSetting(IServiceProvider services, string collectionName)
{

View file

@ -2,9 +2,9 @@ using BotSharp.Plugin.KnowledgeBase.MemVecDb;
using Tensorflow.NumPy;
using static Tensorflow.Binding;
namespace BotSharp.Plugin.KnowledgeBase.Utilities;
namespace BotSharp.Plugin.KnowledgeBase.Helpers;
public static class VectorUtility
public static class VectorHelper
{
public static float[] CalEuclideanDistance(float[] vec, List<VecRecord> records)
{

View file

@ -35,7 +35,7 @@ public class KnowledgeBasePlugin : IBotSharpPlugin
SubMenu = new List<PluginMenuDef>
{
new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"),
new PluginMenuDef("Relations", link: "page/knowledge-base/relations")
new PluginMenuDef("Relationships", link: "page/knowledge-base/relationships")
}
});
return true;

View file

@ -40,7 +40,7 @@ public class MemoryVectorDb : IVectorDb
return new List<VectorCollectionData>();
}
var similarities = VectorUtility.CalCosineSimilarity(vector, _vectors[collectionName]);
var similarities = VectorHelper.CalCosineSimilarity(vector, _vectors[collectionName]);
// var similarities = VectorUtility.CalEuclideanDistance(vector, _vectors[collectionName]);
var results = np.argsort(similarities).ToArray<int>()

View file

@ -33,6 +33,6 @@ public partial class KnowledgeService : IKnowledgeService
private ITextEmbedding GetTextEmbedding(string collection)
{
return KnowledgeSettingUtility.GetTextEmbeddingSetting(_services, collection);
return KnowledgeSettingHelper.GetTextEmbeddingSetting(_services, collection);
}
}

View file

@ -33,4 +33,4 @@ global using BotSharp.Abstraction.Functions.Models;
global using BotSharp.Abstraction.Repositories;
global using BotSharp.Plugin.KnowledgeBase.Services;
global using BotSharp.Plugin.KnowledgeBase.Enum;
global using BotSharp.Plugin.KnowledgeBase.Utilities;
global using BotSharp.Plugin.KnowledgeBase.Helpers;

View file

@ -4,8 +4,8 @@ using BotSharp.Plugin.OpenAI.Providers.Embedding;
using BotSharp.Plugin.OpenAI.Providers.Image;
using BotSharp.Plugin.OpenAI.Providers.Text;
using BotSharp.Plugin.OpenAI.Providers.Chat;
using Microsoft.Extensions.Configuration;
using BotSharp.Plugin.OpenAI.Providers.Audio;
using Microsoft.Extensions.Configuration;
namespace BotSharp.Plugin.OpenAI;
@ -31,7 +31,6 @@ public class OpenAiPlugin : IBotSharpPlugin
services.AddScoped<IChatCompletion, ChatCompletionProvider>();
services.AddScoped<ITextEmbedding, TextEmbeddingProvider>();
services.AddScoped<IImageCompletion, ImageCompletionProvider>();
services.AddScoped<ITextToSpeech, TextToSpeechProvider>();
services.AddScoped<ISpeechToText, SpeechToTextProvider>();
services.AddScoped<IAudioCompletion, AudioCompletionProvider>();
}
}

View file

@ -0,0 +1,53 @@
using OpenAI.Audio;
namespace BotSharp.Plugin.OpenAI.Providers.Audio;
public partial class AudioCompletionProvider
{
public async Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName, string? text = null)
{
var audioClient = ProviderHelper.GetClient(Provider, _model, _services)
.GetAudioClient(_model);
var options = PrepareTranscriptionOptions(text);
var result = await audioClient.TranscribeAudioAsync(audio, audioFileName, options);
return result.Value.Text;
}
private AudioTranscriptionOptions PrepareTranscriptionOptions(string? text)
{
var state = _services.GetRequiredService<IConversationStateService>();
var options = new AudioTranscriptionOptions
{
ResponseFormat = AudioTranscriptionFormat.Verbose,
Granularities = AudioTimestampGranularities.Word | AudioTimestampGranularities.Segment,
Prompt = text
};
return options;
}
private AudioTranscriptionFormat GetTranscriptionResponseFormat(string format)
{
var value = !string.IsNullOrEmpty(format) ? format : "verbose";
AudioTranscriptionFormat retFormat;
switch (value)
{
case "json":
retFormat = AudioTranscriptionFormat.Simple;
break;
case "srt":
retFormat = AudioTranscriptionFormat.Srt;
break;
case "vtt":
retFormat = AudioTranscriptionFormat.Vtt;
break;
default:
retFormat = AudioTranscriptionFormat.Verbose;
break;
}
return retFormat;
}
}

View file

@ -0,0 +1,23 @@
using OpenAI.Audio;
namespace BotSharp.Plugin.OpenAI.Providers.Audio;
public partial class AudioCompletionProvider
{
public async Task<BinaryData> GenerateSpeechFromTextAsync(string text)
{
var audioClient = ProviderHelper.GetClient(Provider, _model, _services)
.GetAudioClient(_model);
var result = await audioClient.GenerateSpeechFromTextAsync(text, GeneratedSpeechVoice.Alloy);
return result.Value;
}
private SpeechGenerationOptions PrepareGenerationOptions()
{
return new SpeechGenerationOptions
{
};
}
}

View file

@ -0,0 +1,21 @@
using OpenAI.Audio;
namespace BotSharp.Plugin.OpenAI.Providers.Audio;
public partial class AudioCompletionProvider : IAudioCompletion
{
private readonly IServiceProvider _services;
public string Provider => "openai";
private string _model;
public AudioCompletionProvider(IServiceProvider service)
{
_services = service;
}
public void SetModelName(string model)
{
_model = model;
}
}

View file

@ -1,50 +0,0 @@
using OpenAI.Audio;
namespace BotSharp.Plugin.OpenAI.Providers.Audio;
public class SpeechToTextProvider : ISpeechToText
{
private readonly IServiceProvider _services;
public string Provider => "openai";
private string? _model;
public SpeechToTextProvider(IServiceProvider service)
{
_services = service;
}
public async Task<string> GenerateTextFromAudioAsync(string filePath)
{
var client = ProviderHelper.GetClient(Provider, _model, _services)
.GetAudioClient(_model);
var options = PrepareOptions();
var result = await client.TranscribeAudioAsync(filePath, options);
return result.Value.Text;
}
public async Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName)
{
var audioClient = ProviderHelper.GetClient(Provider, _model, _services)
.GetAudioClient(_model);
var options = PrepareOptions();
var result = await audioClient.TranscribeAudioAsync(audio, audioFileName, options);
return result.Value.Text;
}
public async Task SetModelName(string model)
{
_model = model;
}
private AudioTranscriptionOptions PrepareOptions()
{
return new AudioTranscriptionOptions
{
ResponseFormat = AudioTranscriptionFormat.Verbose,
Granularities = AudioTimestampGranularities.Word | AudioTimestampGranularities.Segment,
};
}
}

View file

@ -1,31 +0,0 @@
using OpenAI.Audio;
namespace BotSharp.Plugin.OpenAI.Providers.Audio
{
public partial class TextToSpeechProvider : ITextToSpeech
{
private readonly IServiceProvider _services;
public string Provider => "openai";
private string? _model;
public TextToSpeechProvider(
IServiceProvider services)
{
_services = services;
}
public async Task<BinaryData> GenerateSpeechFromTextAsync(string text, ITextToSpeechOptions? options = null)
{
var client = ProviderHelper.GetClient(Provider, _model, _services)
.GetAudioClient(_model);
return await client.GenerateSpeechFromTextAsync(text, GeneratedSpeechVoice.Alloy);
}
public void SetModelName(string model)
{
_model = model;
}
}
}

View file

@ -7,6 +7,24 @@ public partial class TencentCosService
return $"{CONVERSATION_FOLDER}/{conversationId}/attachments/";
}
public IEnumerable<string> GetFiles(string relativePath, string? searchPattern = null)
{
if (string.IsNullOrEmpty(relativePath))
{
return Enumerable.Empty<string>();
}
try
{
return _cosClient.BucketClient.GetDirFiles(relativePath);
}
catch (Exception ex)
{
_logger.LogWarning($"Error when getting files: {ex.Message}\r\n{ex.InnerException}");
return Enumerable.Empty<string>();
}
}
public byte[] GetFileBytes(string fileStorageUrl)
{
try
@ -15,10 +33,10 @@ public partial class TencentCosService
}
catch (Exception ex)
{
_logger.LogWarning($"Error when get file bytes: {ex.Message}\r\n{ex.InnerException}");
}
_logger.LogWarning($"Error when getting file bytes: {ex.Message}\r\n{ex.InnerException}");
return Array.Empty<byte>();
}
}
public bool SaveFileStreamToPath(string filePath, Stream stream)
{

View file

@ -112,11 +112,11 @@ public class TwilioVoiceController : TwilioController
}
else
{
var textToSpeechService = CompletionProvider.GetTextToSpeech(_services, "openai", "tts-1");
var fileService = _services.GetRequiredService<IFileStorageService>();
var data = await textToSpeechService.GenerateSpeechFromTextAsync(indication);
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var data = await completion.GenerateSpeechFromTextAsync(indication);
var fileName = $"indication_{seqNum}.mp3";
await fileService.SaveSpeechFileAsync(conversationId, fileName, data);
await fileStorage.SaveSpeechFileAsync(conversationId, fileName, data);
speechPath = $"twilio/voice/speeches/{conversationId}/{fileName}";
}
response = twilio.ReturnInstructions(speechPath, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 2);

View file

@ -3,7 +3,6 @@ using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Models;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using Task = System.Threading.Tasks.Task;
@ -97,11 +96,11 @@ namespace BotSharp.Plugin.Twilio.Services
async functionExecuted =>
{ }
);
var textToSpeechService = CompletionProvider.GetTextToSpeech(sp, "openai", "tts-1");
var fileService = sp.GetRequiredService<IFileStorageService>();
var data = await textToSpeechService.GenerateSpeechFromTextAsync(reply.Content);
var completion = CompletionProvider.GetAudioCompletion(sp, "openai", "tts-1");
var fileStorage = sp.GetRequiredService<IFileStorageService>();
var data = await completion.GenerateSpeechFromTextAsync(reply.Content);
var fileName = $"reply_{reply.MessageId}.mp3";
await fileService.SaveSpeechFileAsync(message.ConversationId, fileName, data);
await fileStorage.SaveSpeechFileAsync(message.ConversationId, fileName, data);
reply.SpeechFileName = fileName;
reply.Content = null;
await sessionManager.SetAssistantReplyAsync(message.ConversationId, message.SeqNumber, reply);

View file

@ -345,6 +345,7 @@
"BotSharp.Plugin.HttpHandler",
"BotSharp.Plugin.FileHandler",
"BotSharp.Plugin.EmailHandler",
"BotSharp.Plugin.AudioHandler",
"BotSharp.Plugin.TencentCos",
"BotSharp.Plugin.PythonInterpreter"
]