Public Access
Add resilient Azure offline transcription backlog
PR and Push Build/Test / build-and-test (push) Successful in 18m41s
PR and Push Build/Test / build-and-test (push) Successful in 18m41s
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using MeetingAssistant.Transcription;
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
@@ -8,6 +9,7 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
{
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly ILogger<VaultTranscriptStore> logger;
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> fileGates = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public VaultTranscriptStore(IOptions<MeetingAssistantOptions> options, ILogger<VaultTranscriptStore> logger)
|
||||
{
|
||||
@@ -36,9 +38,50 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
return new TranscriptSession(path);
|
||||
}
|
||||
|
||||
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||
public async Task<TranscriptLineReference> AppendLineAsync(
|
||||
TranscriptSession session,
|
||||
string line,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return AppendToBodyAsync(session.TranscriptPath, line + Environment.NewLine, cancellationToken);
|
||||
return await WithFileGateAsync(
|
||||
session.TranscriptPath,
|
||||
() => AppendToBodyAsync(session.TranscriptPath, line, cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ReplaceLineAsync(
|
||||
TranscriptSession session,
|
||||
TranscriptLineReference lineReference,
|
||||
string replacementLine,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await WithFileGateAsync(
|
||||
session.TranscriptPath,
|
||||
async () =>
|
||||
{
|
||||
if (!File.Exists(session.TranscriptPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var content = await File.ReadAllTextAsync(session.TranscriptPath, cancellationToken);
|
||||
var (frontmatter, body) = MeetingArtifactFrontmatterRenderer.Split(content);
|
||||
var lines = SplitLines(body);
|
||||
if (lineReference.BodyLineIndex < 0 ||
|
||||
lineReference.BodyLineIndex >= lines.Count ||
|
||||
!string.Equals(lines[lineReference.BodyLineIndex], lineReference.OriginalLine, StringComparison.Ordinal))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Could not replace transcript line in {TranscriptPath} at body line {BodyLineIndex} because the original line no longer matched",
|
||||
session.TranscriptPath,
|
||||
lineReference.BodyLineIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
lines[lineReference.BodyLineIndex] = replacementLine;
|
||||
await WriteBodyAsync(session.TranscriptPath, frontmatter, string.Join(Environment.NewLine, lines), cancellationToken);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task ReplaceLinesAsync(
|
||||
@@ -46,18 +89,21 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
IReadOnlyList<string> replacementLines,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = File.Exists(session.TranscriptPath)
|
||||
? File.ReadAllText(session.TranscriptPath)
|
||||
: "";
|
||||
var (frontmatter, _) = MeetingArtifactFrontmatterRenderer.Split(existing);
|
||||
var body = "# Meeting Transcript"
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ string.Concat(replacementLines.Select(line => line + Environment.NewLine));
|
||||
var content = string.IsNullOrWhiteSpace(frontmatter)
|
||||
? body
|
||||
: "---" + Environment.NewLine + frontmatter + Environment.NewLine + "---" + Environment.NewLine + Environment.NewLine + body;
|
||||
return File.WriteAllTextAsync(session.TranscriptPath, content, cancellationToken);
|
||||
return WithFileGateAsync(
|
||||
session.TranscriptPath,
|
||||
async () =>
|
||||
{
|
||||
var existing = File.Exists(session.TranscriptPath)
|
||||
? File.ReadAllText(session.TranscriptPath)
|
||||
: "";
|
||||
var (frontmatter, _) = MeetingArtifactFrontmatterRenderer.Split(existing);
|
||||
var body = "# Meeting Transcript"
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ string.Concat(replacementLines.Select(line => line + Environment.NewLine));
|
||||
await WriteBodyAsync(session.TranscriptPath, frontmatter, body, cancellationToken);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpdateMetadataAsync(
|
||||
@@ -66,50 +112,108 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
MeetingNote meetingNote,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var body = File.Exists(session.TranscriptPath)
|
||||
? MeetingArtifactFrontmatterRenderer.Split(await File.ReadAllTextAsync(session.TranscriptPath, cancellationToken)).Body
|
||||
: "# Meeting Transcript" + Environment.NewLine + Environment.NewLine;
|
||||
var frontmatter = MeetingArtifactFrontmatterRenderer.Create(
|
||||
artifacts,
|
||||
meetingNote,
|
||||
MeetingArtifactFrontmatterRenderer.DefaultTitle(meetingNote, "Meeting Transcript"),
|
||||
session.TranscriptPath);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
await WithFileGateAsync(
|
||||
session.TranscriptPath,
|
||||
MeetingArtifactFrontmatterRenderer.Render(frontmatter, body),
|
||||
async () =>
|
||||
{
|
||||
var body = File.Exists(session.TranscriptPath)
|
||||
? MeetingArtifactFrontmatterRenderer.Split(await File.ReadAllTextAsync(session.TranscriptPath, cancellationToken)).Body
|
||||
: "# Meeting Transcript" + Environment.NewLine + Environment.NewLine;
|
||||
var frontmatter = MeetingArtifactFrontmatterRenderer.Create(
|
||||
artifacts,
|
||||
meetingNote,
|
||||
MeetingArtifactFrontmatterRenderer.DefaultTitle(meetingNote, "Meeting Transcript"),
|
||||
session.TranscriptPath);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
session.TranscriptPath,
|
||||
MeetingArtifactFrontmatterRenderer.Render(frontmatter, body),
|
||||
cancellationToken);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task AppendToBodyAsync(
|
||||
private static async Task<TranscriptLineReference> AppendToBodyAsync(
|
||||
string path,
|
||||
string text,
|
||||
string line,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
await File.AppendAllTextAsync(path, text, cancellationToken);
|
||||
return;
|
||||
await File.AppendAllTextAsync(path, line + Environment.NewLine, cancellationToken);
|
||||
return new TranscriptLineReference(path, 0, line);
|
||||
}
|
||||
|
||||
var content = await File.ReadAllTextAsync(path, cancellationToken);
|
||||
var (frontmatter, body) = MeetingArtifactFrontmatterRenderer.Split(content);
|
||||
var bodyLineIndex = GetAppendBodyLineIndex(body);
|
||||
if (string.IsNullOrWhiteSpace(frontmatter))
|
||||
{
|
||||
await File.AppendAllTextAsync(path, text, cancellationToken);
|
||||
return;
|
||||
await File.AppendAllTextAsync(path, line + Environment.NewLine, cancellationToken);
|
||||
return new TranscriptLineReference(path, bodyLineIndex, line);
|
||||
}
|
||||
|
||||
var updated = "---"
|
||||
+ Environment.NewLine
|
||||
+ frontmatter
|
||||
+ Environment.NewLine
|
||||
+ "---"
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ body
|
||||
+ text;
|
||||
await File.WriteAllTextAsync(path, updated, cancellationToken);
|
||||
await WriteBodyAsync(path, frontmatter, body + line + Environment.NewLine, cancellationToken);
|
||||
return new TranscriptLineReference(path, bodyLineIndex, line);
|
||||
}
|
||||
|
||||
private async Task WithFileGateAsync(
|
||||
string path,
|
||||
Func<Task> action,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await WithFileGateAsync(
|
||||
path,
|
||||
async () =>
|
||||
{
|
||||
await action();
|
||||
return true;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T> WithFileGateAsync<T>(
|
||||
string path,
|
||||
Func<Task<T>> action,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var gate = fileGates.GetOrAdd(path, static _ => new SemaphoreSlim(1, 1));
|
||||
await gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteBodyAsync(
|
||||
string path,
|
||||
string frontmatter,
|
||||
string body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var content = string.IsNullOrWhiteSpace(frontmatter)
|
||||
? body
|
||||
: "---" + Environment.NewLine + frontmatter + Environment.NewLine + "---" + Environment.NewLine + Environment.NewLine + body;
|
||||
await File.WriteAllTextAsync(path, content, cancellationToken);
|
||||
}
|
||||
|
||||
private static int GetAppendBodyLineIndex(string body)
|
||||
{
|
||||
var lines = SplitLines(body);
|
||||
return lines.Count > 0 && lines[^1].Length == 0
|
||||
? lines.Count - 1
|
||||
: lines.Count;
|
||||
}
|
||||
|
||||
private static List<string> SplitLines(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Split('\n')
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user