Add transcript line workflow rules
PR and Push Build/Test / build-and-test (push) Failing after 13m27s

This commit is contained in:
2026-06-03 11:24:11 +02:00
parent 50daa88389
commit 40853115c5
20 changed files with 627 additions and 70 deletions
@@ -61,6 +61,25 @@ public sealed class RecordingCoordinatorTests
}
}
private static TranscriptionSegment ParseTranscriptLine(string line)
{
var bracket = line.IndexOf(']');
var rest = bracket >= 0 && bracket + 1 < line.Length
? line[(bracket + 1)..].TrimStart()
: line;
var separator = rest.IndexOf(": ", StringComparison.Ordinal);
if (separator < 0)
{
return new TranscriptionSegment(TimeSpan.Zero, TimeSpan.Zero, "Unknown", rest);
}
return new TranscriptionSegment(
TimeSpan.Zero,
TimeSpan.Zero,
rest[..separator],
rest[(separator + 2)..]);
}
[Fact]
public async Task ToggleStartsStreamingTranscriptionAndSecondToggleStopsIt()
{
@@ -97,6 +116,70 @@ public sealed class RecordingCoordinatorTests
Assert.False(stopped.IsRecording);
}
[Fact]
public async Task TranscriptLineWorkflowRuleTransformsLiveTranscriptBeforePersistingMarkdown()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var rulesPath = Path.Combine(root, "rules.yaml");
Directory.CreateDirectory(root);
await File.WriteAllTextAsync(rulesPath, MeetingWorkflowTestRules.MaskedProfanityRedactionYaml);
var options = new MeetingAssistantOptions
{
Vault =
{
BaseFolder = root,
MeetingNotesFolder = "Notes",
TranscriptsFolder = "Transcripts",
AssistantContextFolder = "Context",
SummariesFolder = "Summaries"
},
Automation =
{
RulesPath = rulesPath
}
};
var audioSource = new ControlledAudioSource();
var noteStore = new MarkdownMeetingNoteStore(
Options.Create(options),
NullLogger<MarkdownMeetingNoteStore>.Instance);
var artifactStore = new MarkdownMeetingArtifactStore(
NullLogger<MarkdownMeetingArtifactStore>.Instance);
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(
new FixedSegmentStreamingTranscriptionProvider(
new TranscriptionSegment(
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(5),
"Guest-1",
"Azure returned ***** here."))),
new VaultTranscriptStore(
Options.Create(options),
NullLogger<VaultTranscriptStore>.Instance),
noteStore,
new CapturingMeetingNoteOpener(),
artifactStore,
new InMemoryRecordedAudioStore(),
new CapturingMeetingSummaryPipeline(),
Options.Create(options),
NullLogger<MeetingRecordingCoordinator>.Instance,
meetingWorkflowEngine: new MeetingWorkflowEngine(
new FileMeetingWorkflowRulesProvider(
NullLogger<FileMeetingWorkflowRulesProvider>.Instance),
noteStore,
artifactStore,
NullLogger<MeetingWorkflowEngine>.Instance));
var started = await coordinator.StartAsync(CancellationToken.None);
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
await WaitUntilAsync(() => FileContainsText(started.TranscriptPath!, "[redacted]"));
await coordinator.StopAsync(CancellationToken.None);
var content = await File.ReadAllTextAsync(started.TranscriptPath!);
Assert.Contains("[00:00:04] Guest-1: Azure returned [redacted] here.", content);
Assert.DoesNotContain("*****", content);
}
[Fact]
public async Task StartCreatesMeetingNoteLinkedToTranscriptAndOpensIt()
{
@@ -1897,7 +1980,7 @@ public sealed class RecordingCoordinatorTests
}
[Fact]
public async Task VaultTranscriptStoreCreatesConfiguredFolderAndAppendsSegments()
public async Task VaultTranscriptStoreCreatesConfiguredFolderAndAppendsLines()
{
var vaultFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var store = new VaultTranscriptStore(
@@ -1908,10 +1991,7 @@ public sealed class RecordingCoordinatorTests
NullLogger<VaultTranscriptStore>.Instance);
var session = await store.CreateSessionAsync(CancellationToken.None);
await store.AppendAsync(
session,
new TranscriptionSegment(TimeSpan.Zero, TimeSpan.FromSeconds(1), "Unknown", "hello vault"),
CancellationToken.None);
await store.AppendLineAsync(session, "[00:00:00] Unknown: hello vault", CancellationToken.None);
Assert.True(Directory.Exists(vaultFolder));
Assert.EndsWith(".md", session.TranscriptPath, StringComparison.Ordinal);
@@ -2079,9 +2159,9 @@ public sealed class RecordingCoordinatorTests
return Task.FromResult(new TranscriptSession(transcriptPath));
}
public Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
{
segments.Add(segment);
segments.Add(ParseTranscriptLine(line));
segmentWritten.TrySetResult();
return Task.CompletedTask;
}
@@ -2108,12 +2188,12 @@ public sealed class RecordingCoordinatorTests
public MeetingNote? MetadataMeetingNote { get; private set; }
public Task ReplaceAsync(
public Task ReplaceLinesAsync(
TranscriptSession session,
IReadOnlyList<TranscriptionSegment> replacementSegments,
IReadOnlyList<string> replacementLines,
CancellationToken cancellationToken)
{
ReplacedSegments = replacementSegments;
ReplacedSegments = replacementLines.Select(ParseTranscriptLine).ToList();
return Task.CompletedTask;
}
@@ -2146,9 +2226,9 @@ public sealed class RecordingCoordinatorTests
return Task.FromResult(new TranscriptSession($"memory-transcript-{index}.md"));
}
public Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
{
AppendHistory.Add(new TranscriptWrite(session, segment));
AppendHistory.Add(new TranscriptWrite(session, ParseTranscriptLine(line)));
Interlocked.Increment(ref appendCount);
appendObserved.TrySetResult();
return Task.CompletedTask;
@@ -2175,12 +2255,14 @@ public sealed class RecordingCoordinatorTests
throw new TimeoutException($"Expected {expectedCount} transcript append(s).");
}
public Task ReplaceAsync(
public Task ReplaceLinesAsync(
TranscriptSession session,
IReadOnlyList<TranscriptionSegment> replacementSegments,
IReadOnlyList<string> replacementLines,
CancellationToken cancellationToken)
{
ReplacementHistory.Add(new TranscriptReplacement(session, replacementSegments));
ReplacementHistory.Add(new TranscriptReplacement(
session,
replacementLines.Select(ParseTranscriptLine).ToList()));
return Task.CompletedTask;
}
@@ -2288,6 +2370,15 @@ public sealed class RecordingCoordinatorTests
Events.Add(workflowEvent);
return Task.CompletedTask;
}
public Task<string> TransformTranscriptLineAsync(
MeetingWorkflowEvent workflowEvent,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
Events.Add(workflowEvent);
return Task.FromResult(workflowEvent.TranscriptLineText ?? "");
}
}
private sealed class CapturingMeetingScreenshotService : IMeetingScreenshotService
@@ -2802,14 +2893,14 @@ public sealed class RecordingCoordinatorTests
return Task.FromResult(new TranscriptSession(Path.Combine(folder, "transcript.md")));
}
public Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task ReplaceAsync(
public Task ReplaceLinesAsync(
TranscriptSession session,
IReadOnlyList<TranscriptionSegment> replacementSegments,
IReadOnlyList<string> replacementLines,
CancellationToken cancellationToken)
{
return Task.CompletedTask;