diff --git a/app/MindWork AI Studio/AudioRecorderHandler.cs b/app/MindWork AI Studio/AudioRecorderHandler.cs new file mode 100644 index 00000000..2ae62501 --- /dev/null +++ b/app/MindWork AI Studio/AudioRecorderHandler.cs @@ -0,0 +1,33 @@ +using AIStudio.Tools.Services; + +namespace AIStudio; + +public static class AudioRecorderHandler +{ + public static void AddAudioRecorderHandlers(this IEndpointRouteBuilder app) + { + var router = app.MapGroup("/audio"); + + router.MapPost("/upload", UploadAudio) + .DisableAntiforgery(); + } + + private static async Task UploadAudio(IFormFile audio, RustService rustService) + { + if (audio.Length == 0) + return Results.BadRequest(); + + var dataDirectory = await rustService.GetDataDirectory(); + var recordingDirectory = Path.Combine(dataDirectory, "audioRecordings"); + if(!Path.Exists(recordingDirectory)) + Directory.CreateDirectory(recordingDirectory); + + var fileName = $"recording_{DateTime.UtcNow:yyyyMMdd_HHmmss}.webm"; + var filePath = Path.Combine(recordingDirectory, fileName); + + await using var stream = File.Create(filePath); + await audio.CopyToAsync(stream); + + return Results.Ok(new { FileName = fileName }); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor b/app/MindWork AI Studio/Layout/MainLayout.razor index 23937719..2145b52a 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor +++ b/app/MindWork AI Studio/Layout/MainLayout.razor @@ -20,6 +20,13 @@ } + + } @@ -41,6 +48,13 @@ } } + + } } diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 064313cf..a578c790 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -39,6 +39,12 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan [Inject] private MudTheme ColorTheme { get; init; } = null!; + [Inject] + private IJSRuntime JsRuntime { get; init; } = null!; + + [Inject] + private HttpClient HttpClient { get; init; } = null!; + private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage; private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;"; @@ -53,6 +59,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan private UpdateResponse? currentUpdateResponse; private MudThemeProvider themeProvider = null!; private bool useDarkMode; + private bool isRecording; private IReadOnlyCollection navItems = []; @@ -341,6 +348,33 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan await this.MessageBus.SendMessage(this, Event.COLOR_THEME_CHANGED); this.StateHasChanged(); } + + private async Task OnRecordingToggled(bool toggled) + { + if (toggled) + { + await this.JsRuntime.InvokeVoidAsync("audioRecorder.start"); + this.isRecording = true; + } + else + { + var base64Audio = await this.JsRuntime.InvokeAsync("audioRecorder.stop"); + this.isRecording = false; + this.StateHasChanged(); + + await this.SendAudioToBackend(base64Audio); + } + } + + private async Task SendAudioToBackend(string base64Audio) + { + var audioBytes = Convert.FromBase64String(base64Audio); + + using var content = new MultipartFormDataContent(); + content.Add(new ByteArrayContent(audioBytes), "audio", "recording.webm"); + + await this.HttpClient.PostAsync("/audio/upload", content); + } #region Implementation of IDisposable diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index cc185180..b59081ec 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -83,7 +83,6 @@ internal sealed class Program } var builder = WebApplication.CreateBuilder(); - builder.WebHost.ConfigureKestrel(kestrelServerOptions => { kestrelServerOptions.ConfigureEndpointDefaults(listenOptions => @@ -193,6 +192,7 @@ internal sealed class Program programLogger.LogInformation("Initialize internal file system."); app.Use(Redirect.HandlerContentAsync); app.Use(FileHandler.HandlerAsync); + app.AddAudioRecorderHandlers(); #if DEBUG app.UseStaticFiles(); diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index aa6b8e2b..ada58411 100644 --- a/app/MindWork AI Studio/wwwroot/app.js +++ b/app/MindWork AI Studio/wwwroot/app.js @@ -25,4 +25,40 @@ window.clearDiv = function (divName) { window.scrollToBottom = function(element) { element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); -} \ No newline at end of file +} + +let mediaRecorder; +let audioChunks = []; + +window.audioRecorder = { + start: async function () { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); + audioChunks = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunks.push(event.data); + } + }; + + mediaRecorder.start(); + }, + + stop: async function () { + return new Promise((resolve) => { + mediaRecorder.onstop = async () => { + const blob = new Blob(audioChunks, { type: 'audio/webm' }); + const arrayBuffer = await blob.arrayBuffer(); + const base64 = btoa( + new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '') + ); + + // Tracks stoppen, damit das Mic-Icon verschwindet + mediaRecorder.stream.getTracks().forEach(track => track.stop()); + resolve(base64); + }; + mediaRecorder.stop(); + }); + } +}; \ No newline at end of file