diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a5254585..e899d79a 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2725,6 +2725,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Content creation" -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Useful assistants" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Voice recording has been disabled for this session because audio playback could not be initialized on the client." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Failed to create the transcription provider." @@ -2734,6 +2737,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Failed to -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Stop recording and start transcription" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Voice recording is unavailable because the client could not initialize audio playback." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start recording your voice for a transcription" diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor b/app/MindWork AI Studio/Components/VoiceRecorder.razor index e247f439..a99afd14 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor @@ -1,9 +1,7 @@ -@using AIStudio.Settings.DataModel - @namespace AIStudio.Components @inherits MSGComponentBase -@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)) +@if (this.ShouldRenderVoiceRecording) { @if (this.isTranscribing || this.isPreparing) @@ -16,6 +14,7 @@ ToggledChanged="@this.OnRecordingToggled" Icon="@Icons.Material.Filled.Mic" ToggledIcon="@Icons.Material.Filled.Stop" + Disabled="@(!this.IsVoiceRecordingAvailable)" Color="Color.Primary" ToggledColor="Color.Error"/> } diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs index 73a95e8d..686656dd 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Provider; +using AIStudio.Settings.DataModel; using AIStudio.Tools.MIME; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; @@ -21,24 +22,25 @@ public partial class VoiceRecorder : MSGComponentBase [Inject] private ISnackbar Snackbar { get; init; } = null!; + [Inject] + private VoiceRecordingAvailabilityService VoiceRecordingAvailabilityService { get; init; } = null!; + #region Overrides of MSGComponentBase protected override async Task OnInitializedAsync() { // Register for global shortcut events: - this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED]); + this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]); await base.OnInitializedAsync(); + } - try - { - // Initialize sound effects. This "warms up" the AudioContext and preloads all sounds for reliable playback: - await this.JsRuntime.InvokeVoidAsync("initSoundEffects"); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize sound effects."); - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && this.ShouldRenderVoiceRecording) + await this.EnsureSoundEffectsAvailableAsync("during the first interactive render"); + + await base.OnAfterRenderAsync(firstRender); } protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default @@ -54,6 +56,10 @@ public partial class VoiceRecorder : MSGComponentBase } break; + + case Event.VOICE_RECORDING_AVAILABILITY_CHANGED: + this.StateHasChanged(); + break; } } @@ -62,6 +68,12 @@ public partial class VoiceRecorder : MSGComponentBase /// private async Task ToggleRecordingFromShortcut() { + if (!this.IsVoiceRecordingAvailable) + { + this.Logger.LogDebug("Ignoring shortcut: voice recording is unavailable in the current session."); + return; + } + // Don't allow toggle if transcription is in progress or preparing: if (this.isTranscribing || this.isPreparing) { @@ -85,27 +97,38 @@ public partial class VoiceRecorder : MSGComponentBase private string? finalRecordingPath; private DotNetObjectReference? dotNetReference; - 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 bool ShouldRenderVoiceRecording => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) + && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider); + + private bool IsVoiceRecordingAvailable => this.ShouldRenderVoiceRecording + && this.VoiceRecordingAvailabilityService.IsAvailable; + + private string Tooltip => !this.VoiceRecordingAvailabilityService.IsAvailable + ? T("Voice recording is unavailable because the client could not initialize audio playback.") + : 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) { if (toggled) { + if (!this.IsVoiceRecordingAvailable) + { + this.Logger.LogDebug("Ignoring recording start: voice recording is unavailable in the current session."); + return; + } + this.isPreparing = true; this.StateHasChanged(); - - try + + if (!await this.EnsureSoundEffectsAvailableAsync("before starting audio recording")) { - // Warm up sound effects: - await this.JsRuntime.InvokeVoidAsync("initSoundEffects"); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize sound effects."); + this.isPreparing = false; + this.StateHasChanged(); + return; } var mimeTypes = GetPreferredMimeTypes( @@ -416,11 +439,66 @@ public partial class VoiceRecorder : MSGComponentBase } } - private sealed class AudioRecordingResult + private async Task EnsureSoundEffectsAvailableAsync(string context) { - public string MimeType { get; init; } = string.Empty; + if (!this.ShouldRenderVoiceRecording) + return false; - public bool ChangedMimeType { get; init; } + if (!this.VoiceRecordingAvailabilityService.IsAvailable) + return false; + + try + { + var result = await this.JsRuntime.InvokeAsync("initSoundEffects"); + if (result.Success) + return true; + + var failureDetails = BuildSoundEffectsFailureDetails(result); + this.Logger.LogError("Failed to initialize sound effects {Context}. {FailureDetails}", context, failureDetails); + await this.DisableVoiceRecordingAsync(failureDetails); + } + catch (JSDisconnectedException ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}. The JS runtime disconnected.", context); + await this.DisableVoiceRecordingAsync("The JS runtime disconnected while initializing audio playback."); + } + catch (OperationCanceledException ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}. The interop call was canceled.", context); + await this.DisableVoiceRecordingAsync("The interop call for audio playback initialization was canceled."); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}.", context); + await this.DisableVoiceRecordingAsync(ex.Message); + } + + return false; + } + + private async Task DisableVoiceRecordingAsync(string reason) + { + if (!this.VoiceRecordingAvailabilityService.TryDisable(reason)) + return; + + this.Logger.LogWarning("Voice recording was disabled for the current session. Reason: {Reason}", reason); + await this.MessageBus.SendWarning(new(Icons.Material.Filled.MicOff, this.T("Voice recording has been disabled for this session because audio playback could not be initialized on the client."))); + await this.SendMessage(Event.VOICE_RECORDING_AVAILABILITY_CHANGED, reason); + this.StateHasChanged(); + } + + private static string BuildSoundEffectsFailureDetails(SoundEffectsInitializationResult result) + { + var details = new List(); + if (result.FailedPaths.Length > 0) + details.Add($"Failed sound files: {string.Join(", ", result.FailedPaths)}."); + + if (!string.IsNullOrWhiteSpace(result.ErrorMessage)) + details.Add($"Client error: {result.ErrorMessage}"); + + return details.Count > 0 + ? string.Join(" ", details) + : "The client did not provide additional details."; } #region Overrides of MSGComponentBase @@ -440,4 +518,4 @@ public partial class VoiceRecorder : MSGComponentBase } #endregion -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index bc520ce8..0a94f7a7 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2727,6 +2727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Erstellung von In -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Nützliche Assistenten" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Die Sprachaufnahme wurde für diese Sitzung deaktiviert, da die Audiowiedergabe auf dem Client nicht initialisiert werden konnte." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Der Anbieter für die Transkription konnte nicht erstellt werden." @@ -2736,6 +2739,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Audioaufn -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Aufnahme beenden und Transkription starten" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Die Sprachaufnahme ist nicht verfügbar, da der Client die Audiowiedergabe nicht initialisieren konnte." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Beginnen Sie mit der Aufnahme Ihrer Stimme für eine Transkription" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index c92c1ff3..2849c6d2 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2727,6 +2727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Content creation" -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Useful assistants" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Voice recording has been disabled for this session because audio playback could not be initialized on the client." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Failed to create the transcription provider." @@ -2736,6 +2739,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Failed to -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Stop recording and start transcription" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Voice recording is unavailable because the client could not initialize audio playback." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start recording your voice for a transcription" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index ba765962..f19344d6 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -169,6 +169,7 @@ internal sealed class Program builder.Services.AddMudMarkdownClipboardService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddTransient(); diff --git a/app/MindWork AI Studio/Tools/AudioRecordingResult.cs b/app/MindWork AI Studio/Tools/AudioRecordingResult.cs new file mode 100644 index 00000000..cdde82ac --- /dev/null +++ b/app/MindWork AI Studio/Tools/AudioRecordingResult.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools; + +public sealed class AudioRecordingResult +{ + public string MimeType { get; init; } = string.Empty; + + public bool ChangedMimeType { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 4c90725d..6e899a79 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -17,6 +17,7 @@ public enum Event SHOW_SUCCESS, TAURI_EVENT_RECEIVED, RUST_SERVICE_UNAVAILABLE, + VOICE_RECORDING_AVAILABILITY_CHANGED, // Update events: USER_SEARCH_FOR_UPDATE, diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index f0bca207..7d701670 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -15,6 +15,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv CONFIGURATION_CHANGED, STARTUP_COMPLETED, PLUGINS_RELOADED, + VOICE_RECORDING_AVAILABILITY_CHANGED, } private readonly SemaphoreSlim registrationSemaphore = new(1, 1); @@ -22,20 +23,23 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private readonly SettingsManager settingsManager; private readonly MessageBus messageBus; private readonly RustService rustService; + private readonly VoiceRecordingAvailabilityService voiceRecordingAvailabilityService; public GlobalShortcutService( ILogger logger, SettingsManager settingsManager, MessageBus messageBus, - RustService rustService) + RustService rustService, + VoiceRecordingAvailabilityService voiceRecordingAvailabilityService) { this.logger = logger; this.settingsManager = settingsManager; this.messageBus = messageBus; this.rustService = rustService; + this.voiceRecordingAvailabilityService = voiceRecordingAvailabilityService; this.messageBus.RegisterComponent(this); - this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED]); + this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -75,6 +79,13 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv await this.RegisterAllShortcuts(ShortcutSyncSource.PLUGINS_RELOADED); break; + + case Event.VOICE_RECORDING_AVAILABILITY_CHANGED: + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.VOICE_RECORDING_AVAILABILITY_CHANGED); + break; } } @@ -152,8 +163,9 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private bool IsShortcutAllowed(Shortcut name) => name switch { // Voice recording is a preview feature: - Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager), - + Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager) + && this.voiceRecordingAvailabilityService.IsAvailable, + // Other shortcuts are always allowed: _ => true, }; diff --git a/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs new file mode 100644 index 00000000..010176c7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools.Services; + +public sealed class VoiceRecordingAvailabilityService +{ + private readonly Lock stateLock = new(); + + public bool IsAvailable { get; private set; } = true; + + public string? DisableReason { get; private set; } + + public bool TryDisable(string reason) + { + lock (this.stateLock) + { + if (!this.IsAvailable) + return false; + + this.IsAvailable = false; + this.DisableReason = reason; + return true; + } + } +} diff --git a/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs b/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs new file mode 100644 index 00000000..70ec2c10 --- /dev/null +++ b/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools; + +public sealed class SoundEffectsInitializationResult +{ + public bool Success { get; init; } + + public string[] FailedPaths { get; init; } = []; + + public string? ErrorMessage { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/audio.js b/app/MindWork AI Studio/wwwroot/audio.js index 689bc50f..4e9f40b5 100644 --- a/app/MindWork AI Studio/wwwroot/audio.js +++ b/app/MindWork AI Studio/wwwroot/audio.js @@ -21,59 +21,68 @@ const SOUND_EFFECT_PATHS = [ '/sounds/transcription_done.ogg' ]; +function createSoundEffectsInitResult(success, failedPaths = [], errorMessage = null) { + return { + success: success, + failedPaths: failedPaths, + errorMessage: errorMessage + }; +} + // Initialize the audio context with low-latency settings. // Should be called from a user interaction (click, keypress) // to satisfy browser autoplay policies: window.initSoundEffects = async function() { - if (soundEffectContext && soundEffectContext.state !== 'closed') { - // Already initialized, just ensure it's running: - if (soundEffectContext.state === 'suspended') { - await soundEffectContext.resume(); - } - - return; - } - try { - // Create the context with the interactive latency hint for the lowest latency: - soundEffectContext = new (window.AudioContext || window.webkitAudioContext)({ - latencyHint: 'interactive' - }); + if (soundEffectContext && soundEffectContext.state !== 'closed') { + // Already initialized, just ensure it's running: + if (soundEffectContext.state === 'suspended') { + await soundEffectContext.resume(); + } + } else { + // Create the context with the interactive latency hint for the lowest latency: + soundEffectContext = new (window.AudioContext || window.webkitAudioContext)({ + latencyHint: 'interactive' + }); - // Resume immediately (needed for Safari/macOS): - if (soundEffectContext.state === 'suspended') { - await soundEffectContext.resume(); + // Resume immediately (needed for Safari/macOS): + if (soundEffectContext.state === 'suspended') { + await soundEffectContext.resume(); + } + + // Reset the queue timing: + nextAvailablePlayTime = 0; + + // + // Play a very short silent buffer to "warm up" the audio pipeline. + // This helps prevent the first real sound from being cut off: + // + const silentBuffer = soundEffectContext.createBuffer(1, 1, soundEffectContext.sampleRate); + const silentSource = soundEffectContext.createBufferSource(); + silentSource.buffer = silentBuffer; + silentSource.connect(soundEffectContext.destination); + silentSource.start(0); + + console.log('Sound effects - AudioContext initialized with latency:', soundEffectContext.baseLatency); } - // Reset the queue timing: - nextAvailablePlayTime = 0; - - // - // Play a very short silent buffer to "warm up" the audio pipeline. - // This helps prevent the first real sound from being cut off: - // - const silentBuffer = soundEffectContext.createBuffer(1, 1, soundEffectContext.sampleRate); - const silentSource = soundEffectContext.createBufferSource(); - silentSource.buffer = silentBuffer; - silentSource.connect(soundEffectContext.destination); - silentSource.start(0); - - console.log('Sound effects - AudioContext initialized with latency:', soundEffectContext.baseLatency); - // Preload all sound effects in parallel: if (!soundEffectsPreloaded) { - await window.preloadSoundEffects(); + return await window.preloadSoundEffects(); } + + return createSoundEffectsInitResult(true); } catch (error) { console.warn('Failed to initialize sound effects:', error); + return createSoundEffectsInitResult(false, [], error?.message || String(error)); } }; // Preload all sound effect files into the cache: window.preloadSoundEffects = async function() { if (soundEffectsPreloaded) { - return; + return createSoundEffectsInitResult(true); } // Ensure that the context exists: @@ -84,10 +93,15 @@ window.preloadSoundEffects = async function() { } console.log('Sound effects - preloading', SOUND_EFFECT_PATHS.length, 'sound files...'); + const failedPaths = []; const preloadPromises = SOUND_EFFECT_PATHS.map(async (soundPath) => { try { const response = await fetch(soundPath); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await soundEffectContext.decodeAudioData(arrayBuffer); soundEffectCache.set(soundPath, audioBuffer); @@ -95,12 +109,20 @@ window.preloadSoundEffects = async function() { console.log('Sound effects - preloaded:', soundPath, 'duration:', audioBuffer.duration.toFixed(2), 's'); } catch (error) { console.warn('Sound effects - failed to preload:', soundPath, error); + failedPaths.push(soundPath); } }); await Promise.all(preloadPromises); - soundEffectsPreloaded = true; - console.log('Sound effects - all files preloaded'); + soundEffectsPreloaded = failedPaths.length === 0; + + if (soundEffectsPreloaded) { + console.log('Sound effects - all files preloaded'); + return createSoundEffectsInitResult(true); + } + + console.warn('Sound effects - preload finished with failures:', failedPaths); + return createSoundEffectsInitResult(false, failedPaths, 'One or more sound effects could not be loaded.'); }; window.playSound = async function(soundPath) { diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index ed3c9834..11a01004 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -16,4 +16,5 @@ - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. - Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize. - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. +- Fixed an issue with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file