diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor b/app/MindWork AI Studio/Components/VoiceRecorder.razor
new file mode 100644
index 00000000..e205e8fc
--- /dev/null
+++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor
@@ -0,0 +1,14 @@
+@using AIStudio.Settings.DataModel
+
+@namespace AIStudio.Components
+@inherits ComponentBase
+
+@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
+{
+
+}
diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
new file mode 100644
index 00000000..380efc50
--- /dev/null
+++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
@@ -0,0 +1,191 @@
+using AIStudio.Settings;
+using AIStudio.Tools.MIME;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components;
+
+public partial class VoiceRecorder : IDisposable
+{
+ [Inject]
+ private ILogger Logger { get; init; } = null!;
+
+ [Inject]
+ private IJSRuntime JsRuntime { get; init; } = null!;
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ [Inject]
+ private SettingsManager SettingsManager { get; set; } = null!;
+
+ private bool isRecording;
+ private FileStream? currentRecordingStream;
+ private string? currentRecordingPath;
+ private string? currentRecordingMimeType;
+ private DotNetObjectReference? dotNetReference;
+
+ private async Task OnRecordingToggled(bool toggled)
+ {
+ if (toggled)
+ {
+ var mimeTypes = GetPreferredMimeTypes(
+ Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
+ Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
+ Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
+ Builder.Create().UseAudio().UseSubtype(AudioSubtype.AIFF).Build(),
+ Builder.Create().UseAudio().UseSubtype(AudioSubtype.WAV).Build(),
+ Builder.Create().UseAudio().UseSubtype(AudioSubtype.FLAC).Build()
+ );
+
+ this.Logger.LogInformation("Starting audio recording with preferred MIME types: {PreferredMimeTypes}", string.Join(", ", mimeTypes));
+
+ // Create a DotNetObjectReference to pass to JavaScript:
+ this.dotNetReference = DotNetObjectReference.Create(this);
+
+ // Initialize the file stream for writing chunks:
+ await this.InitializeRecordingStream();
+
+ var mimeTypeStrings = mimeTypes.ToStringArray();
+ var actualMimeType = await this.JsRuntime.InvokeAsync("audioRecorder.start", this.dotNetReference, mimeTypeStrings);
+
+ // Store the MIME type for later use:
+ this.currentRecordingMimeType = actualMimeType;
+
+ this.Logger.LogInformation("Audio recording started with MIME type: {ActualMimeType}", actualMimeType);
+ this.isRecording = true;
+ }
+ else
+ {
+ var result = await this.JsRuntime.InvokeAsync("audioRecorder.stop");
+ if (result.ChangedMimeType)
+ this.Logger.LogWarning("The recorded audio MIME type was changed to '{ResultMimeType}'.", result.MimeType);
+
+ // Close and finalize the recording stream:
+ await this.FinalizeRecordingStream();
+
+ this.isRecording = false;
+ this.StateHasChanged();
+ }
+ }
+
+ private static MIMEType[] GetPreferredMimeTypes(params MIMEType[] mimeTypes)
+ {
+ // Default list if no parameters provided:
+ if (mimeTypes.Length is 0)
+ {
+ var audioBuilder = Builder.Create().UseAudio();
+ return
+ [
+ audioBuilder.UseSubtype(AudioSubtype.WEBM).Build(),
+ audioBuilder.UseSubtype(AudioSubtype.OGG).Build(),
+ audioBuilder.UseSubtype(AudioSubtype.MP4).Build(),
+ audioBuilder.UseSubtype(AudioSubtype.MPEG).Build(),
+ ];
+ }
+
+ return mimeTypes;
+ }
+
+ private async Task InitializeRecordingStream()
+ {
+ var dataDirectory = await this.RustService.GetDataDirectory();
+ var recordingDirectory = Path.Combine(dataDirectory, "audioRecordings");
+ if (!Directory.Exists(recordingDirectory))
+ Directory.CreateDirectory(recordingDirectory);
+
+ var fileName = $"recording_{DateTime.UtcNow:yyyyMMdd_HHmmss}.audio";
+ this.currentRecordingPath = Path.Combine(recordingDirectory, fileName);
+ this.currentRecordingStream = new FileStream(this.currentRecordingPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true);
+
+ this.Logger.LogInformation("Initialized audio recording stream: {RecordingPath}", this.currentRecordingPath);
+ }
+
+ [JSInvokable]
+ public async Task OnAudioChunkReceived(byte[] chunkBytes)
+ {
+ if (this.currentRecordingStream is null)
+ {
+ this.Logger.LogWarning("Received audio chunk but no recording stream is active.");
+ return;
+ }
+
+ try
+ {
+ await this.currentRecordingStream.WriteAsync(chunkBytes);
+ await this.currentRecordingStream.FlushAsync();
+
+ this.Logger.LogDebug("Wrote {ByteCount} bytes to recording stream.", chunkBytes.Length);
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, "Error writing audio chunk to stream.");
+ }
+ }
+
+ private async Task FinalizeRecordingStream()
+ {
+ if (this.currentRecordingStream is not null)
+ {
+ await this.currentRecordingStream.FlushAsync();
+ await this.currentRecordingStream.DisposeAsync();
+ this.currentRecordingStream = null;
+
+ // Rename the file with the correct extension based on MIME type:
+ if (this.currentRecordingPath is not null && this.currentRecordingMimeType is not null)
+ {
+ var extension = GetFileExtension(this.currentRecordingMimeType);
+ var newPath = Path.ChangeExtension(this.currentRecordingPath, extension);
+
+ if (File.Exists(this.currentRecordingPath))
+ {
+ File.Move(this.currentRecordingPath, newPath, overwrite: true);
+ this.Logger.LogInformation("Finalized audio recording: {RecordingPath}", newPath);
+ }
+ }
+ }
+
+ this.currentRecordingPath = null;
+ this.currentRecordingMimeType = null;
+
+ // Dispose the .NET reference:
+ this.dotNetReference?.Dispose();
+ this.dotNetReference = null;
+ }
+
+ private static string GetFileExtension(string mimeType)
+ {
+ var baseMimeType = mimeType.Split(';')[0].Trim().ToLowerInvariant();
+ return baseMimeType switch
+ {
+ "audio/webm" => ".webm",
+ "audio/ogg" => ".ogg",
+ "audio/mp4" => ".m4a",
+ "audio/mpeg" => ".mp3",
+ "audio/wav" => ".wav",
+ "audio/x-wav" => ".wav",
+ _ => ".audio" // Fallback
+ };
+ }
+
+ private sealed class AudioRecordingResult
+ {
+ public string MimeType { get; init; } = string.Empty;
+
+ public bool ChangedMimeType { get; init; }
+ }
+
+ public void Dispose()
+ {
+ // Clean up recording resources if still active:
+ if (this.currentRecordingStream is not null)
+ {
+ this.currentRecordingStream.Dispose();
+ this.currentRecordingStream = null;
+ }
+
+ this.dotNetReference?.Dispose();
+ this.dotNetReference = null;
+ }
+}
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor b/app/MindWork AI Studio/Layout/MainLayout.razor
index f98c1ce1..6aef66dc 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor
@@ -1,4 +1,6 @@
@using AIStudio.Settings.DataModel
+@using AIStudio.Components
+
@using Microsoft.AspNetCore.Components.Routing
@using MudBlazor
@@ -20,16 +22,7 @@
}
-
- @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
- {
-
- }
+
}
@@ -51,16 +44,7 @@
}
}
-
- @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
- {
-
- }
+
}
}
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index 1252e5e5..89fcd418 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -57,11 +57,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private UpdateResponse? currentUpdateResponse;
private MudThemeProvider themeProvider = null!;
private bool useDarkMode;
- private bool isRecording;
- private FileStream? currentRecordingStream;
- private string? currentRecordingPath;
- private string? currentRecordingMimeType;
- private DotNetObjectReference? dotNetReference;
private IReadOnlyCollection navItems = [];
@@ -351,174 +346,11 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
this.StateHasChanged();
}
- private async Task OnRecordingToggled(bool toggled)
- {
- if (toggled)
- {
- var mimeTypes = GetPreferredMimeTypes(
- Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
- Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
- Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
- Builder.Create().UseAudio().UseSubtype(AudioSubtype.AIFF).Build(),
- Builder.Create().UseAudio().UseSubtype(AudioSubtype.WAV).Build(),
- Builder.Create().UseAudio().UseSubtype(AudioSubtype.FLAC).Build()
- );
-
- this.Logger.LogInformation("Starting audio recording with preferred MIME types: {PreferredMimeTypes}", string.Join(", ", mimeTypes));
-
- // Create a DotNetObjectReference to pass to JavaScript:
- this.dotNetReference = DotNetObjectReference.Create(this);
-
- // Initialize the file stream for writing chunks:
- await this.InitializeRecordingStream();
-
- var mimeTypeStrings = mimeTypes.ToStringArray();
- var actualMimeType = await this.JsRuntime.InvokeAsync("audioRecorder.start", this.dotNetReference, mimeTypeStrings);
-
- // Store the MIME type for later use:
- this.currentRecordingMimeType = actualMimeType;
-
- this.Logger.LogInformation("Audio recording started with MIME type: {ActualMimeType}", actualMimeType);
- this.isRecording = true;
- }
- else
- {
- var result = await this.JsRuntime.InvokeAsync("audioRecorder.stop");
- if(result.ChangedMimeType)
- this.Logger.LogWarning("The recorded audio MIME type was changed to '{ResultMimeType}'.", result.MimeType);
-
- // Close and finalize the recording stream:
- await this.FinalizeRecordingStream();
-
- this.isRecording = false;
- this.StateHasChanged();
- }
- }
-
- private static MIMEType[] GetPreferredMimeTypes(params MIMEType[] mimeTypes)
- {
- // Default list if no parameters provided:
- if (mimeTypes.Length is 0)
- {
- var audioBuilder = Builder.Create().UseAudio();
- return
- [
- audioBuilder.UseSubtype(AudioSubtype.WEBM).Build(),
- audioBuilder.UseSubtype(AudioSubtype.OGG).Build(),
- audioBuilder.UseSubtype(AudioSubtype.MP4).Build(),
- audioBuilder.UseSubtype(AudioSubtype.MPEG).Build(),
- ];
- }
-
- return mimeTypes;
- }
-
- private async Task InitializeRecordingStream()
- {
- var dataDirectory = await this.RustService.GetDataDirectory();
- var recordingDirectory = Path.Combine(dataDirectory, "audioRecordings");
- if(!Directory.Exists(recordingDirectory))
- Directory.CreateDirectory(recordingDirectory);
-
- var fileName = $"recording_{DateTime.UtcNow:yyyyMMdd_HHmmss}.audio";
- this.currentRecordingPath = Path.Combine(recordingDirectory, fileName);
- this.currentRecordingStream = new FileStream(this.currentRecordingPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true);
-
- this.Logger.LogInformation("Initialized audio recording stream: {RecordingPath}", this.currentRecordingPath);
- }
-
- [JSInvokable]
- public async Task OnAudioChunkReceived(byte[] chunkBytes)
- {
- if (this.currentRecordingStream is null)
- {
- this.Logger.LogWarning("Received audio chunk but no recording stream is active.");
- return;
- }
-
- try
- {
- await this.currentRecordingStream.WriteAsync(chunkBytes);
- await this.currentRecordingStream.FlushAsync();
-
- this.Logger.LogDebug("Wrote {ByteCount} bytes to recording stream.", chunkBytes.Length);
- }
- catch (Exception ex)
- {
- this.Logger.LogError(ex, "Error writing audio chunk to stream.");
- }
- }
-
- private async Task FinalizeRecordingStream()
- {
- if (this.currentRecordingStream is not null)
- {
- await this.currentRecordingStream.FlushAsync();
- await this.currentRecordingStream.DisposeAsync();
- this.currentRecordingStream = null;
-
- // Rename the file with the correct extension based on MIME type:
- if (this.currentRecordingPath is not null && this.currentRecordingMimeType is not null)
- {
- var extension = GetFileExtension(this.currentRecordingMimeType);
- var newPath = Path.ChangeExtension(this.currentRecordingPath, extension);
-
- if (File.Exists(this.currentRecordingPath))
- {
- File.Move(this.currentRecordingPath, newPath, overwrite: true);
- this.Logger.LogInformation("Finalized audio recording: {RecordingPath}", newPath);
- }
- }
- }
-
- this.currentRecordingPath = null;
- this.currentRecordingMimeType = null;
-
- // Dispose the .NET reference:
- this.dotNetReference?.Dispose();
- this.dotNetReference = null;
- }
-
- private static string GetFileExtension(string mimeType)
- {
- // Codec-Parameter entfernen für Matching
- var baseMimeType = mimeType.Split(';')[0].Trim().ToLowerInvariant();
-
- return baseMimeType switch
- {
- "audio/webm" => ".webm",
- "audio/ogg" => ".ogg",
- "audio/mp4" => ".m4a",
- "audio/mpeg" => ".mp3",
- "audio/wav" => ".wav",
- "audio/x-wav" => ".wav",
- _ => ".audio" // Fallback
- };
- }
-
- private sealed class AudioRecordingResult
- {
-
- public string MimeType { get; init; } = string.Empty;
-
- public bool ChangedMimeType { get; init; }
- }
-
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
-
- // Clean up recording resources if still active:
- if (this.currentRecordingStream is not null)
- {
- this.currentRecordingStream.Dispose();
- this.currentRecordingStream = null;
- }
-
- this.dotNetReference?.Dispose();
- this.dotNetReference = null;
}
#endregion