mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-02-15 19:21:36 +00:00
WIP: First implementation of audio recording
This commit is contained in:
parent
47975870b4
commit
b1336abbfd
33
app/MindWork AI Studio/AudioRecorderHandler.cs
Normal file
33
app/MindWork AI Studio/AudioRecorderHandler.cs
Normal file
@ -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<IResult> 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,13 @@
|
|||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
}
|
}
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
|
<MudToggleIconButton Toggled="@this.isRecording"
|
||||||
|
ToggledChanged="@this.OnRecordingToggled"
|
||||||
|
Icon="@Icons.Material.Filled.Mic"
|
||||||
|
ToggledIcon="@Icons.Material.Filled.Stop"
|
||||||
|
Color="Color.Primary"
|
||||||
|
ToggledColor="Color.Error" />
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
</MudDrawerContainer>
|
</MudDrawerContainer>
|
||||||
}
|
}
|
||||||
@ -41,6 +48,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
|
<MudToggleIconButton Toggled="@this.isRecording"
|
||||||
|
ToggledChanged="@this.OnRecordingToggled"
|
||||||
|
Icon="@Icons.Material.Filled.Mic"
|
||||||
|
ToggledIcon="@Icons.Material.Filled.Stop"
|
||||||
|
Color="Color.Primary"
|
||||||
|
ToggledColor="Color.Error" />
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,12 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
|||||||
[Inject]
|
[Inject]
|
||||||
private MudTheme ColorTheme { get; init; } = null!;
|
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 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;";
|
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 UpdateResponse? currentUpdateResponse;
|
||||||
private MudThemeProvider themeProvider = null!;
|
private MudThemeProvider themeProvider = null!;
|
||||||
private bool useDarkMode;
|
private bool useDarkMode;
|
||||||
|
private bool isRecording;
|
||||||
|
|
||||||
private IReadOnlyCollection<NavBarItem> navItems = [];
|
private IReadOnlyCollection<NavBarItem> navItems = [];
|
||||||
|
|
||||||
@ -342,6 +349,33 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
|||||||
this.StateHasChanged();
|
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<string>("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
|
#region Implementation of IDisposable
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@ -83,7 +83,6 @@ internal sealed class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder();
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
|
||||||
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
|
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
|
||||||
{
|
{
|
||||||
kestrelServerOptions.ConfigureEndpointDefaults(listenOptions =>
|
kestrelServerOptions.ConfigureEndpointDefaults(listenOptions =>
|
||||||
@ -193,6 +192,7 @@ internal sealed class Program
|
|||||||
programLogger.LogInformation("Initialize internal file system.");
|
programLogger.LogInformation("Initialize internal file system.");
|
||||||
app.Use(Redirect.HandlerContentAsync);
|
app.Use(Redirect.HandlerContentAsync);
|
||||||
app.Use(FileHandler.HandlerAsync);
|
app.Use(FileHandler.HandlerAsync);
|
||||||
|
app.AddAudioRecorderHandlers();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|||||||
@ -26,3 +26,39 @@ window.clearDiv = function (divName) {
|
|||||||
window.scrollToBottom = function(element) {
|
window.scrollToBottom = function(element) {
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
|
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user