Implemented the transcription API (#623)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-01-11 16:02:28 +01:00
1 parent b94614b0ab
commit 529986837e
42 files changed
+700 -138

No files matched your search

@@ -15,6 +15,6 @@
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
<MudTooltip Text="@this.ToggleVisibilityTooltip">
<MudIconButton Icon="@this.InputTypeIcon" OnClick="() => this.ToggleVisibility()"/>
<MudIconButton Icon="@this.InputTypeIcon" OnClick="@(() => this.ToggleVisibility())"/>
</MudTooltip>
</MudStack>
@@ -100,13 +100,13 @@ public partial class SettingsPanelEmbeddings : SettingsPanelBase
if (dialogResult is null || dialogResult.Canceled)
return;
var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider);
var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider, SecretStoreType.EMBEDDING_PROVIDER);
if(deleteSecretResponse.Success)
{
this.SettingsManager.ConfigurationData.EmbeddingProviders.Remove(provider);
await this.SettingsManager.StoreSettings();
}
await this.UpdateEmbeddingProviders();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
@@ -107,7 +107,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
if (dialogResult is null || dialogResult.Canceled)
return;
var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider);
var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider, SecretStoreType.LLM_PROVIDER);
if(deleteSecretResponse.Success)
{
this.SettingsManager.ConfigurationData.Providers.Remove(provider);
@@ -10,7 +10,7 @@
@T("Configured Transcription Providers")
</MudText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the \"Configure providers\" section.")
@T("With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure providers' section.")
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.TranscriptionProviders" Hover="@true" Class="border-dashed border rounded-lg">
@@ -100,13 +100,13 @@ public partial class SettingsPanelTranscription : SettingsPanelBase
if (dialogResult is null || dialogResult.Canceled)
return;
var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider);
var deleteSecretResponse = await this.RustService.DeleteAPIKey(provider, SecretStoreType.TRANSCRIPTION_PROVIDER);
if(deleteSecretResponse.Success)
{
this.SettingsManager.ConfigurationData.TranscriptionProviders.Remove(provider);
await this.SettingsManager.StoreSettings();
}
await this.UpdateTranscriptionProviders();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
@@ -6,11 +6,18 @@
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider))
{
<MudTooltip Text="@this.Tooltip">
<MudToggleIconButton Toggled="@this.isRecording"
ToggledChanged="@this.OnRecordingToggled"
Icon="@Icons.Material.Filled.Mic"
ToggledIcon="@Icons.Material.Filled.Stop"
Color="Color.Primary"
ToggledColor="Color.Error"/>
@if (this.isTranscribing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Color="Color.Primary"/>
}
else
{
<MudToggleIconButton Toggled="@this.isRecording"
ToggledChanged="@this.OnRecordingToggled"
Icon="@Icons.Material.Filled.Mic"
ToggledIcon="@Icons.Material.Filled.Stop"
Color="Color.Primary"
ToggledColor="Color.Error"/>
}
</MudTooltip>
}
@@ -1,3 +1,4 @@
using AIStudio.Provider;
using AIStudio.Tools.MIME;
using AIStudio.Tools.Services;
@@ -9,21 +10,30 @@ public partial class VoiceRecorder : MSGComponentBase
{
[Inject]
private ILogger<VoiceRecorder> Logger { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
private uint numReceivedChunks;
private bool isRecording;
private bool isTranscribing;
private FileStream? currentRecordingStream;
private string? currentRecordingPath;
private string? currentRecordingMimeType;
private string? finalRecordingPath;
private DotNetObjectReference<VoiceRecorder>? dotNetReference;
private string Tooltip => this.isRecording ? T("Stop recording and start transcription") : T("Start recording your voice for a transcription");
private string Tooltip => this.isTranscribing
? T("Transcription in progress...")
: this.isRecording
? T("Stop recording and start transcription")
: T("Start recording your voice for a transcription");
private async Task OnRecordingToggled(bool toggled)
{
@@ -66,6 +76,10 @@ public partial class VoiceRecorder : MSGComponentBase
this.isRecording = false;
this.StateHasChanged();
// Start transcription if we have a recording and a configured provider:
if (this.finalRecordingPath is not null)
await this.TranscribeRecordingAsync();
}
}
@@ -127,6 +141,7 @@ public partial class VoiceRecorder : MSGComponentBase
private async Task FinalizeRecordingStream()
{
this.finalRecordingPath = null;
if (this.currentRecordingStream is not null)
{
await this.currentRecordingStream.FlushAsync();
@@ -142,6 +157,7 @@ public partial class VoiceRecorder : MSGComponentBase
if (File.Exists(this.currentRecordingPath))
{
File.Move(this.currentRecordingPath, newPath, overwrite: true);
this.finalRecordingPath = newPath;
this.Logger.LogInformation("Finalized audio recording over {NumChunks} streamed audio chunks to the file '{RecordingPath}'.", this.numReceivedChunks, newPath);
}
}
@@ -170,6 +186,114 @@ public partial class VoiceRecorder : MSGComponentBase
};
}
private async Task TranscribeRecordingAsync()
{
if (this.finalRecordingPath is null)
return;
this.isTranscribing = true;
this.StateHasChanged();
try
{
// Get the configured transcription provider ID:
var transcriptionProviderId = this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider;
if (string.IsNullOrWhiteSpace(transcriptionProviderId))
{
this.Logger.LogWarning("No transcription provider is configured.");
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, this.T("No transcription provider is configured.")));
return;
}
// Find the transcription provider in the list of configured providers:
var transcriptionProviderSettings = this.SettingsManager.ConfigurationData.TranscriptionProviders
.FirstOrDefault(x => x.Id == transcriptionProviderId);
if (transcriptionProviderSettings is null)
{
this.Logger.LogWarning("The configured transcription provider with ID '{ProviderId}' was not found.", transcriptionProviderId);
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, this.T("The configured transcription provider was not found.")));
return;
}
// Check the confidence level:
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(Tools.Components.NONE);
var providerConfidence = transcriptionProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager);
if (providerConfidence.Level < minimumLevel)
{
this.Logger.LogWarning(
"The configured transcription provider '{ProviderName}' has a confidence level of '{ProviderLevel}', which is below the minimum required level of '{MinimumLevel}'.",
transcriptionProviderSettings.Name,
providerConfidence.Level,
minimumLevel);
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, this.T("The configured transcription provider does not meet the minimum confidence level.")));
return;
}
// Create the provider instance:
var provider = transcriptionProviderSettings.CreateProvider();
if (provider.Provider is LLMProviders.NONE)
{
this.Logger.LogError("Failed to create the transcription provider instance.");
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, this.T("Failed to create the transcription provider.")));
return;
}
// Call the transcription API:
this.Logger.LogInformation("Starting transcription with provider '{ProviderName}' and model '{ModelName}'.", transcriptionProviderSettings.Name, transcriptionProviderSettings.Model.DisplayName);
var transcribedText = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
if (string.IsNullOrWhiteSpace(transcribedText))
{
this.Logger.LogWarning("The transcription result is empty.");
await this.MessageBus.SendWarning(new(Icons.Material.Filled.VoiceChat, this.T("The transcription result is empty.")));
return;
}
// Remove trailing and leading whitespace:
transcribedText = transcribedText.Trim();
// Replace line breaks with spaces:
transcribedText = transcribedText.Replace("\r", " ").Replace("\n", " ");
// Replace two spaces with a single space:
transcribedText = transcribedText.Replace(" ", " ");
this.Logger.LogInformation("Transcription completed successfully. Result length: {Length} characters.", transcribedText.Length);
// Play the transcription done sound effect:
await this.JsRuntime.InvokeVoidAsync("playSound", "/sounds/transcription_done.ogg");
// Copy the transcribed text to the clipboard:
await this.RustService.CopyText2Clipboard(this.Snackbar, transcribedText);
// Delete the recording file:
try
{
if (File.Exists(this.finalRecordingPath))
{
File.Delete(this.finalRecordingPath);
this.Logger.LogInformation("Deleted the recording file '{RecordingPath}'.", this.finalRecordingPath);
}
}
catch (Exception ex)
{
this.Logger.LogError(ex, "Failed to delete the recording file '{RecordingPath}'.", this.finalRecordingPath);
}
}
catch (Exception ex)
{
this.Logger.LogError(ex, "An error occurred during transcription.");
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, this.T("An error occurred during transcription.")));
}
finally
{
this.finalRecordingPath = null;
this.isTranscribing = false;
this.StateHasChanged();
}
}
private sealed class AudioRecordingResult
{
public string MimeType { get; init; } = string.Empty;