Public Access
159 lines
5.2 KiB
C#
159 lines
5.2 KiB
C#
using Microsoft.Extensions.Options;
|
|
using NAudio.Wave;
|
|
|
|
namespace MeetingAssistant.Recording;
|
|
|
|
public sealed class TemporaryRecordedAudioStore : IRecordedAudioStore
|
|
{
|
|
private readonly MeetingAssistantOptions options;
|
|
private readonly ILogger<TemporaryRecordedAudioStore> logger;
|
|
private readonly IOfflineTranscriptionBacklog offlineTranscriptionBacklog;
|
|
|
|
public TemporaryRecordedAudioStore(
|
|
IOptions<MeetingAssistantOptions> options,
|
|
ILogger<TemporaryRecordedAudioStore> logger,
|
|
IOfflineTranscriptionBacklog? offlineTranscriptionBacklog = null)
|
|
{
|
|
this.options = options.Value;
|
|
this.logger = logger;
|
|
this.offlineTranscriptionBacklog = offlineTranscriptionBacklog ?? NoopOfflineTranscriptionBacklog.Instance;
|
|
}
|
|
|
|
public Task<IRecordedAudioSink> CreateSessionAsync(CancellationToken cancellationToken)
|
|
{
|
|
return CreateSessionAsync(options, cancellationToken);
|
|
}
|
|
|
|
public Task<IRecordedAudioSink> CreateSessionAsync(
|
|
MeetingAssistantOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var folder = GetTemporaryRecordingsFolder(options);
|
|
Directory.CreateDirectory(folder);
|
|
|
|
var path = Path.Combine(folder, $"{DateTimeOffset.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid():N}-recording.wav");
|
|
var format = new WaveFormat(options.Recording.SampleRate, 16, options.Recording.Channels);
|
|
logger.LogInformation("Created temporary meeting recording file {RecordingPath}", path);
|
|
|
|
return Task.FromResult<IRecordedAudioSink>(new TemporaryRecordedAudioSink(path, format, logger));
|
|
}
|
|
|
|
public async Task DeleteStaleRecordingsAsync(CancellationToken cancellationToken)
|
|
{
|
|
var folder = GetTemporaryRecordingsFolder(options);
|
|
if (!Directory.Exists(folder))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var queuedAudioPaths = (await offlineTranscriptionBacklog.ListAsync(cancellationToken))
|
|
.Select(item => item.AudioPath)
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var path in Directory.EnumerateFiles(folder, "*.wav", SearchOption.TopDirectoryOnly))
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (queuedAudioPaths.Contains(path))
|
|
{
|
|
logger.LogInformation("Keeping queued offline transcription recording file {RecordingPath}", path);
|
|
continue;
|
|
}
|
|
|
|
DeleteFile(path);
|
|
}
|
|
}
|
|
|
|
private static string GetTemporaryRecordingsFolder(MeetingAssistantOptions options)
|
|
{
|
|
return VaultPath.Resolve(options.Recording.TemporaryRecordingsFolder);
|
|
}
|
|
|
|
private void DeleteFile(string path)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(path);
|
|
logger.LogInformation("Deleted temporary meeting recording file {RecordingPath}", path);
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
}
|
|
catch (DirectoryNotFoundException)
|
|
{
|
|
}
|
|
catch (IOException exception)
|
|
{
|
|
logger.LogWarning(exception, "Could not delete temporary meeting recording file {RecordingPath}", path);
|
|
}
|
|
catch (UnauthorizedAccessException exception)
|
|
{
|
|
logger.LogWarning(exception, "Could not delete temporary meeting recording file {RecordingPath}", path);
|
|
}
|
|
}
|
|
|
|
private sealed class TemporaryRecordedAudioSink : IRecordedAudioSink
|
|
{
|
|
private readonly WaveFileWriter writer;
|
|
private readonly ILogger logger;
|
|
private bool completed;
|
|
private bool disposed;
|
|
|
|
public TemporaryRecordedAudioSink(string audioPath, WaveFormat format, ILogger logger)
|
|
{
|
|
AudioPath = audioPath;
|
|
this.logger = logger;
|
|
writer = new WaveFileWriter(audioPath, format);
|
|
}
|
|
|
|
public string AudioPath { get; }
|
|
|
|
public Task AppendAsync(AudioChunk chunk, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
writer.Write(chunk.Pcm, 0, chunk.Pcm.Length);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task CompleteAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (completed)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
writer.Flush();
|
|
completed = true;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
if (disposed)
|
|
{
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
writer.Dispose();
|
|
disposed = true;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public async Task DeleteAsync(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
await DisposeAsync();
|
|
try
|
|
{
|
|
File.Delete(AudioPath);
|
|
logger.LogInformation("Deleted temporary meeting recording file {RecordingPath}", AudioPath);
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
}
|
|
catch (DirectoryNotFoundException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|