mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 23:52:11 +00:00
Improved audio recording error handling and chunk processing
This commit is contained in:
parent
c159875351
commit
43a508b4de
@ -137,6 +137,7 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
var mimeTypes = GetPreferredMimeTypes(
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.WEBM).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP4).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.AIFF).Build(),
|
||||
@ -171,6 +172,7 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
|
||||
// Clean up the recording stream if starting failed:
|
||||
await this.FinalizeRecordingStream();
|
||||
await this.ReleaseMicrophoneAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -179,11 +181,13 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
}
|
||||
else
|
||||
{
|
||||
var recordingStoppedSuccessfully = false;
|
||||
try
|
||||
{
|
||||
var result = await this.JsRuntime.InvokeAsync<AudioRecordingResult>("audioRecorder.stop");
|
||||
if (result.ChangedMimeType)
|
||||
this.Logger.LogWarning("The recorded audio MIME type was changed to '{ResultMimeType}'.", result.MimeType);
|
||||
recordingStoppedSuccessfully = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -197,9 +201,20 @@ 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();
|
||||
if (!recordingStoppedSuccessfully || this.finalRecordingPath is null)
|
||||
{
|
||||
if (recordingStoppedSuccessfully)
|
||||
{
|
||||
this.Logger.LogWarning("The audio recorder did not produce any data.");
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.MicOff, this.T("Failed to stop audio recording.")));
|
||||
}
|
||||
|
||||
this.DeleteFinalRecording();
|
||||
await this.ReleaseMicrophoneAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.TranscribeRecordingAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,6 +271,7 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Error writing audio chunk to stream.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,17 +284,23 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
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)
|
||||
if (this.currentRecordingPath is not null && File.Exists(this.currentRecordingPath))
|
||||
{
|
||||
var extension = GetFileExtension(this.currentRecordingMimeType);
|
||||
var newPath = Path.ChangeExtension(this.currentRecordingPath, extension);
|
||||
var fileSize = new FileInfo(this.currentRecordingPath).Length;
|
||||
|
||||
if (File.Exists(this.currentRecordingPath))
|
||||
// Rename non-empty recordings with the correct extension based on MIME type:
|
||||
if (fileSize > 0 && this.currentRecordingMimeType is not null)
|
||||
{
|
||||
var extension = GetFileExtension(this.currentRecordingMimeType);
|
||||
var newPath = Path.ChangeExtension(this.currentRecordingPath, extension);
|
||||
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);
|
||||
this.Logger.LogInformation("Finalized audio recording over {NumChunks} streamed audio chunks to the file '{RecordingPath}' with {FileSize} bytes.", this.numReceivedChunks, newPath, fileSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Logger.LogWarning("Discarding an audio recording with {FileSize} bytes and MIME type '{MimeType}'.", fileSize, this.currentRecordingMimeType);
|
||||
File.Delete(this.currentRecordingPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -375,22 +397,31 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
finally
|
||||
{
|
||||
await this.ReleaseMicrophoneAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(this.finalRecordingPath))
|
||||
File.Delete(this.finalRecordingPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Failed to delete the recording file '{RecordingPath}'.", this.finalRecordingPath);
|
||||
}
|
||||
this.finalRecordingPath = null;
|
||||
this.DeleteFinalRecording();
|
||||
this.isTranscribing = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteFinalRecording()
|
||||
{
|
||||
var recordingPath = this.finalRecordingPath;
|
||||
this.finalRecordingPath = null;
|
||||
|
||||
if (recordingPath is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(recordingPath))
|
||||
File.Delete(recordingPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Failed to delete the recording file '{RecordingPath}'.", recordingPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReleaseMicrophoneAsync()
|
||||
{
|
||||
// Wait a moment for any queued sounds to finish playing, then release the microphone.
|
||||
@ -486,4 +517,4 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,6 +184,9 @@ let mediaRecorder;
|
||||
let actualRecordingMimeType;
|
||||
let changedMimeType = false;
|
||||
let pendingChunkUploads = 0;
|
||||
let chunkUploadPromise = Promise.resolve();
|
||||
let chunkUploadError = null;
|
||||
let recordingError = null;
|
||||
|
||||
// Store the media stream so we can close the microphone later:
|
||||
let activeMediaStream = null;
|
||||
@ -246,48 +249,65 @@ window.audioRecorder = {
|
||||
actualRecordingMimeType = mediaRecorder.mimeType;
|
||||
console.log('Audio recording - actual mime type used by the browser: ', actualRecordingMimeType);
|
||||
|
||||
const actualBaseMimeType = actualRecordingMimeType.split(';')[0].trim().toLowerCase();
|
||||
|
||||
// Check the list of desired mime types against the actual one:
|
||||
if (!desiredMimeTypes.includes(actualRecordingMimeType)) {
|
||||
if (!desiredMimeTypes.some(type => type.split(';')[0].trim().toLowerCase() === actualBaseMimeType)) {
|
||||
changedMimeType = true;
|
||||
console.warn(`Audio recording - requested mime types ('${desiredMimeTypes.join(', ')}') do not include the actual mime type used by the browser ('${actualRecordingMimeType}').`);
|
||||
} else {
|
||||
changedMimeType = false;
|
||||
}
|
||||
|
||||
// Reset the pending uploads counter:
|
||||
// Reset the upload and recorder state:
|
||||
pendingChunkUploads = 0;
|
||||
chunkUploadPromise = Promise.resolve();
|
||||
chunkUploadError = null;
|
||||
recordingError = null;
|
||||
|
||||
// Stream each chunk directly to .NET as it becomes available:
|
||||
mediaRecorder.ondataavailable = async (event) => {
|
||||
// Stream each chunk directly to .NET in recording order as it becomes available:
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
pendingChunkUploads++;
|
||||
try {
|
||||
const arrayBuffer = await event.data.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
await dotnetRef.invokeMethodAsync('OnAudioChunkReceived', uint8Array);
|
||||
} catch (error) {
|
||||
console.error('Error sending audio chunk to .NET:', error);
|
||||
} finally {
|
||||
pendingChunkUploads--;
|
||||
}
|
||||
chunkUploadPromise = chunkUploadPromise
|
||||
.then(async () => {
|
||||
const arrayBuffer = await event.data.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
await dotnetRef.invokeMethodAsync('OnAudioChunkReceived', uint8Array);
|
||||
})
|
||||
.catch(error => {
|
||||
chunkUploadError ??= error;
|
||||
console.error('Error sending audio chunk to .NET:', error);
|
||||
})
|
||||
.finally(() => pendingChunkUploads--);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.start(3000); // read the recorded data in 3-second chunks
|
||||
mediaRecorder.onerror = event => {
|
||||
recordingError ??= event.error || new Error('The media recorder failed.');
|
||||
console.error('Audio recording - MediaRecorder error:', recordingError);
|
||||
};
|
||||
|
||||
if (actualBaseMimeType === 'audio/mp4') {
|
||||
// WebKitGTK does not reliably produce MP4 data when a timeslice is used. Let it
|
||||
// finalize one complete M4A blob when stop() is called instead.
|
||||
mediaRecorder.start();
|
||||
} else {
|
||||
mediaRecorder.start(3000); // read the recorded data in 3-second chunks
|
||||
}
|
||||
|
||||
return actualRecordingMimeType;
|
||||
},
|
||||
|
||||
stop: async function () {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Add an event listener to handle the stop event:
|
||||
mediaRecorder.onstop = async () => {
|
||||
|
||||
// Wait for all pending chunk uploads to complete before finalizing:
|
||||
console.log(`Audio recording - waiting for ${pendingChunkUploads} pending uploads.`);
|
||||
while (pendingChunkUploads > 0) {
|
||||
await new Promise(r => setTimeout(r, 10)); // wait 10 ms before checking again
|
||||
}
|
||||
await chunkUploadPromise;
|
||||
|
||||
console.log('Audio recording - all chunks uploaded, finalizing.');
|
||||
|
||||
@ -303,6 +323,12 @@ window.audioRecorder = {
|
||||
// Call window.audioRecorder.releaseMicrophone() after the last sound has played.
|
||||
//
|
||||
|
||||
const error = recordingError || chunkUploadError;
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to process data here anymore, just signal completion:
|
||||
resolve({
|
||||
mimeType: actualRecordingMimeType,
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
||||
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience.
|
||||
- Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder.
|
||||
- Fixed voice recording not starting on Linux.
|
||||
- Fixed voice recording and transcription on Linux.
|
||||
- Fixed being able to switch document analysis policies while an analysis or media transcription was still in progress.
|
||||
- Upgraded Rust to v1.97.0.
|
||||
- Upgraded Tauri to v2.11.5.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user