Public Access
Use consistent meeting artifact filenames
PR and Push Build/Test / build-and-test (push) Successful in 10m14s
PR and Push Build/Test / build-and-test (push) Successful in 10m14s
This commit is contained in:
@@ -57,6 +57,19 @@ public sealed class MeetingNoteStoreTests
|
|||||||
Assert.Equal("[[../Summaries/20260519-summary|Summary]]", loaded.Frontmatter.Summary);
|
Assert.Equal("[[../Summaries/20260519-summary|Summary]]", loaded.Frontmatter.Summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StoreNamesGeneratedMeetingNoteWithMinutePrecision()
|
||||||
|
{
|
||||||
|
var (_, store) = CreateStore();
|
||||||
|
var saved = await store.SaveAsync(
|
||||||
|
MeetingNoteTemplate.Create(
|
||||||
|
title: "Leadership Sync",
|
||||||
|
startTime: DateTimeOffset.Parse("2026-05-19T10:03:42+02:00")),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("20260519-1003-note.md", Path.GetFileName(saved.Path));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FrontmatterUpdatePreservesExistingUserNotesBody()
|
public async Task FrontmatterUpdatePreservesExistingUserNotesBody()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ public sealed class RecordingCoordinatorTests
|
|||||||
var audioSource = new ControlledAudioSource();
|
var audioSource = new ControlledAudioSource();
|
||||||
var transcriptStore = new InMemoryTranscriptStore("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md");
|
var transcriptStore = new InMemoryTranscriptStore("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md");
|
||||||
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md");
|
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md");
|
||||||
|
var clock = new ManualMeetingInactivityClock(DateTimeOffset.Parse("2026-05-19T10:03:42+02:00"));
|
||||||
var noteOpener = new CapturingMeetingNoteOpener();
|
var noteOpener = new CapturingMeetingNoteOpener();
|
||||||
var artifactStore = new InMemoryMeetingArtifactStore();
|
var artifactStore = new InMemoryMeetingArtifactStore();
|
||||||
var audioArchive = new InMemoryRecordedAudioStore();
|
var audioArchive = new InMemoryRecordedAudioStore();
|
||||||
@@ -206,22 +207,21 @@ public sealed class RecordingCoordinatorTests
|
|||||||
SummariesFolder = "C:\\Vault\\Meetings\\Summaries"
|
SummariesFolder = "C:\\Vault\\Meetings\\Summaries"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
NullLogger<MeetingRecordingCoordinator>.Instance);
|
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||||
|
inactivityClock: clock);
|
||||||
|
|
||||||
var status = await coordinator.StartAsync(CancellationToken.None);
|
var status = await coordinator.StartAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(status.IsRecording);
|
Assert.True(status.IsRecording);
|
||||||
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", status.MeetingNotePath);
|
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", status.MeetingNotePath);
|
||||||
Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", noteStore.SavedNote?.Frontmatter.Transcript);
|
Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", noteStore.SavedNote?.Frontmatter.Transcript);
|
||||||
Assert.StartsWith("C:\\Vault\\Meetings\\Assistant Context\\", noteStore.SavedNote?.Frontmatter.AssistantContext, StringComparison.Ordinal);
|
Assert.Equal("C:\\Vault\\Meetings\\Assistant Context\\20260519-1003-context.md", noteStore.SavedNote?.Frontmatter.AssistantContext);
|
||||||
Assert.EndsWith("-assistant-context.md", noteStore.SavedNote?.Frontmatter.AssistantContext, StringComparison.Ordinal);
|
Assert.Equal("C:\\Vault\\Meetings\\Summaries\\20260519-1003-summary.md", noteStore.SavedNote?.Frontmatter.Summary);
|
||||||
Assert.StartsWith("C:\\Vault\\Meetings\\Summaries\\", noteStore.SavedNote?.Frontmatter.Summary, StringComparison.Ordinal);
|
|
||||||
Assert.EndsWith("-summary.md", noteStore.SavedNote?.Frontmatter.Summary, StringComparison.Ordinal);
|
|
||||||
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", noteOpener.OpenedPath);
|
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", noteOpener.OpenedPath);
|
||||||
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", artifactStore.CreatedArtifacts?.MeetingNotePath);
|
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", artifactStore.CreatedArtifacts?.MeetingNotePath);
|
||||||
Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", artifactStore.CreatedArtifacts?.TranscriptPath);
|
Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", artifactStore.CreatedArtifacts?.TranscriptPath);
|
||||||
Assert.StartsWith("C:\\Vault\\Meetings\\Assistant Context\\", artifactStore.CreatedArtifacts?.AssistantContextPath, StringComparison.Ordinal);
|
Assert.Equal("C:\\Vault\\Meetings\\Assistant Context\\20260519-1003-context.md", artifactStore.CreatedArtifacts?.AssistantContextPath);
|
||||||
Assert.StartsWith("C:\\Vault\\Meetings\\Summaries\\", artifactStore.CreatedArtifacts?.SummaryPath, StringComparison.Ordinal);
|
Assert.Equal("C:\\Vault\\Meetings\\Summaries\\20260519-1003-summary.md", artifactStore.CreatedArtifacts?.SummaryPath);
|
||||||
Assert.Equal(noteStore.SavedNote?.Frontmatter.Title, artifactStore.ContextMeetingNote?.Frontmatter.Title);
|
Assert.Equal(noteStore.SavedNote?.Frontmatter.Title, artifactStore.ContextMeetingNote?.Frontmatter.Title);
|
||||||
Assert.Equal(noteStore.SavedNote?.Frontmatter.StartTime, artifactStore.ContextMeetingNote?.Frontmatter.StartTime);
|
Assert.Equal(noteStore.SavedNote?.Frontmatter.StartTime, artifactStore.ContextMeetingNote?.Frontmatter.StartTime);
|
||||||
|
|
||||||
@@ -1101,6 +1101,7 @@ public sealed class RecordingCoordinatorTests
|
|||||||
var defaultRoot = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "default");
|
var defaultRoot = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "default");
|
||||||
var englishRoot = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "english");
|
var englishRoot = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "english");
|
||||||
var launchProfiles = CreateLaunchProfiles(defaultRoot, englishRoot);
|
var launchProfiles = CreateLaunchProfiles(defaultRoot, englishRoot);
|
||||||
|
var clock = new ManualMeetingInactivityClock(DateTimeOffset.Parse("2026-05-19T10:03:42+02:00"));
|
||||||
var summaryPipeline = new CapturingMeetingSummaryPipeline();
|
var summaryPipeline = new CapturingMeetingSummaryPipeline();
|
||||||
var coordinator = new MeetingRecordingCoordinator(
|
var coordinator = new MeetingRecordingCoordinator(
|
||||||
new CapturedChunkThenCancelAudioSource(new AudioChunk([1, 0], 16000, 1)),
|
new CapturedChunkThenCancelAudioSource(new AudioChunk([1, 0], 16000, 1)),
|
||||||
@@ -1113,15 +1114,16 @@ public sealed class RecordingCoordinatorTests
|
|||||||
summaryPipeline,
|
summaryPipeline,
|
||||||
Options.Create(launchProfiles.GetRequiredProfile(null).Options),
|
Options.Create(launchProfiles.GetRequiredProfile(null).Options),
|
||||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||||
launchProfiles: launchProfiles);
|
launchProfiles: launchProfiles,
|
||||||
|
inactivityClock: clock);
|
||||||
|
|
||||||
var started = await coordinator.StartAsync("english", CancellationToken.None);
|
var started = await coordinator.StartAsync("english", CancellationToken.None);
|
||||||
await coordinator.StopAsync(CancellationToken.None);
|
await coordinator.StopAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.StartsWith(Path.Combine(englishRoot, "Transcripts"), started.TranscriptPath, StringComparison.OrdinalIgnoreCase);
|
Assert.Equal(Path.Combine(englishRoot, "Transcripts", "20260519-1003-transcript.md"), started.TranscriptPath);
|
||||||
Assert.StartsWith(Path.Combine(englishRoot, "Notes"), started.MeetingNotePath, StringComparison.OrdinalIgnoreCase);
|
Assert.Equal(Path.Combine(englishRoot, "Notes", "20260519-1003-note.md"), started.MeetingNotePath);
|
||||||
Assert.StartsWith(Path.Combine(englishRoot, "Context"), started.AssistantContextPath, StringComparison.OrdinalIgnoreCase);
|
Assert.Equal(Path.Combine(englishRoot, "Context", "20260519-1003-context.md"), started.AssistantContextPath);
|
||||||
Assert.StartsWith(Path.Combine(englishRoot, "Summaries"), started.SummaryPath, StringComparison.OrdinalIgnoreCase);
|
Assert.Equal(Path.Combine(englishRoot, "Summaries", "20260519-1003-summary.md"), started.SummaryPath);
|
||||||
Assert.Equal("english-summary-model", summaryPipeline.Options?.Agent.Model);
|
Assert.Equal("english-summary-model", summaryPipeline.Options?.Agent.Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2015,6 +2017,26 @@ public sealed class RecordingCoordinatorTests
|
|||||||
Assert.DoesNotContain("transcript:", content);
|
Assert.DoesNotContain("transcript:", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VaultTranscriptStoreNamesGeneratedTranscriptWithMinutePrecision()
|
||||||
|
{
|
||||||
|
var vaultFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
||||||
|
var options = new MeetingAssistantOptions
|
||||||
|
{
|
||||||
|
Vault = new VaultOptions { TranscriptsFolder = vaultFolder }
|
||||||
|
};
|
||||||
|
var store = new VaultTranscriptStore(
|
||||||
|
Options.Create(options),
|
||||||
|
NullLogger<VaultTranscriptStore>.Instance);
|
||||||
|
|
||||||
|
var session = await store.CreateSessionAsync(
|
||||||
|
options,
|
||||||
|
DateTimeOffset.Parse("2026-05-19T10:03:42+02:00"),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("20260519-1003-transcript.md", Path.GetFileName(session.TranscriptPath));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TemporaryRecordedAudioStoreCreatesConfiguredFolderAndWritesPcmWav()
|
public async Task TemporaryRecordedAudioStoreCreatesConfiguredFolderAndWritesPcmWav()
|
||||||
{
|
{
|
||||||
@@ -2159,6 +2181,14 @@ public sealed class RecordingCoordinatorTests
|
|||||||
return Task.FromResult(new TranscriptSession(transcriptPath));
|
return Task.FromResult(new TranscriptSession(transcriptPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<TranscriptSession> CreateSessionAsync(
|
||||||
|
MeetingAssistantOptions options,
|
||||||
|
DateTimeOffset startedAt,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return CreateSessionAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
segments.Add(ParseTranscriptLine(line));
|
segments.Add(ParseTranscriptLine(line));
|
||||||
@@ -2226,6 +2256,14 @@ public sealed class RecordingCoordinatorTests
|
|||||||
return Task.FromResult(new TranscriptSession($"memory-transcript-{index}.md"));
|
return Task.FromResult(new TranscriptSession($"memory-transcript-{index}.md"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<TranscriptSession> CreateSessionAsync(
|
||||||
|
MeetingAssistantOptions options,
|
||||||
|
DateTimeOffset startedAt,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return CreateSessionAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
AppendHistory.Add(new TranscriptWrite(session, ParseTranscriptLine(line)));
|
AppendHistory.Add(new TranscriptWrite(session, ParseTranscriptLine(line)));
|
||||||
@@ -2887,10 +2925,13 @@ public sealed class RecordingCoordinatorTests
|
|||||||
|
|
||||||
public Task<TranscriptSession> CreateSessionAsync(
|
public Task<TranscriptSession> CreateSessionAsync(
|
||||||
MeetingAssistantOptions options,
|
MeetingAssistantOptions options,
|
||||||
|
DateTimeOffset startedAt,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var folder = VaultPath.Resolve(options.Vault, options.Vault.TranscriptsFolder);
|
var folder = VaultPath.Resolve(options.Vault, options.Vault.TranscriptsFolder);
|
||||||
return Task.FromResult(new TranscriptSession(Path.Combine(folder, "transcript.md")));
|
return Task.FromResult(new TranscriptSession(Path.Combine(
|
||||||
|
folder,
|
||||||
|
MeetingArtifactFileNames.Create(startedAt, MeetingArtifactFileNames.Transcript))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||||
@@ -2931,7 +2972,16 @@ public sealed class RecordingCoordinatorTests
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var folder = VaultPath.Resolve(options.Vault, options.Vault.MeetingNotesFolder);
|
var folder = VaultPath.Resolve(options.Vault, options.Vault.MeetingNotesFolder);
|
||||||
savedNote = note with { Path = string.IsNullOrWhiteSpace(note.Path) ? Path.Combine(folder, "meeting.md") : note.Path };
|
savedNote = note with
|
||||||
|
{
|
||||||
|
Path = string.IsNullOrWhiteSpace(note.Path)
|
||||||
|
? Path.Combine(
|
||||||
|
folder,
|
||||||
|
MeetingArtifactFileNames.Create(
|
||||||
|
note.Frontmatter.StartTime ?? DateTimeOffset.Now,
|
||||||
|
MeetingArtifactFileNames.Note))
|
||||||
|
: note.Path
|
||||||
|
};
|
||||||
return Task.FromResult(savedNote);
|
return Task.FromResult(savedNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
|
|||||||
Directory.CreateDirectory(folder);
|
Directory.CreateDirectory(folder);
|
||||||
|
|
||||||
var path = string.IsNullOrWhiteSpace(note.Path)
|
var path = string.IsNullOrWhiteSpace(note.Path)
|
||||||
? Path.Combine(folder, $"{DateTimeOffset.Now:yyyyMMdd-HHmmss-fffffff}-{Slugify(note.Frontmatter.Title)}.md")
|
? Path.Combine(folder, MeetingArtifactFileNames.Create(note.Frontmatter.StartTime ?? DateTimeOffset.Now, MeetingArtifactFileNames.Note))
|
||||||
: note.Path;
|
: note.Path;
|
||||||
var frontmatter = PrepareFrontmatter(note.Frontmatter, path);
|
var frontmatter = PrepareFrontmatter(note.Frontmatter, path);
|
||||||
var content = Render(frontmatter, note.UserNotes);
|
var content = Render(frontmatter, note.UserNotes);
|
||||||
@@ -108,16 +108,6 @@ public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
|
|||||||
return string.Join(Environment.NewLine, lines);
|
return string.Join(Environment.NewLine, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Slugify(string value)
|
|
||||||
{
|
|
||||||
var slug = new string(value
|
|
||||||
.ToLowerInvariant()
|
|
||||||
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
|
|
||||||
.ToArray());
|
|
||||||
slug = string.Join("-", slug.Split('-', StringSplitOptions.RemoveEmptyEntries));
|
|
||||||
return string.IsNullOrWhiteSpace(slug) ? "meeting" : slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EscapeScalar(string value)
|
private static string EscapeScalar(string value)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(value) ? "\"\"" : EscapeListItem(value);
|
return string.IsNullOrWhiteSpace(value) ? "\"\"" : EscapeListItem(value);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace MeetingAssistant.MeetingNotes;
|
||||||
|
|
||||||
|
public static class MeetingArtifactFileNames
|
||||||
|
{
|
||||||
|
public const string Note = "note";
|
||||||
|
public const string Context = "context";
|
||||||
|
public const string Transcript = "transcript";
|
||||||
|
public const string Summary = "summary";
|
||||||
|
|
||||||
|
public static string Create(DateTimeOffset startedAt, string artifactType)
|
||||||
|
{
|
||||||
|
return $"{startedAt:yyyyMMdd-HHmm}-{artifactType}.md";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,8 @@ public interface ITranscriptStore
|
|||||||
|
|
||||||
Task<TranscriptSession> CreateSessionAsync(
|
Task<TranscriptSession> CreateSessionAsync(
|
||||||
MeetingAssistantOptions options,
|
MeetingAssistantOptions options,
|
||||||
CancellationToken cancellationToken)
|
DateTimeOffset startedAt,
|
||||||
{
|
CancellationToken cancellationToken);
|
||||||
return CreateSessionAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
|
Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ public sealed class MeetingRecordingCoordinator
|
|||||||
|
|
||||||
var launchProfile = ResolveProfile(launchProfileName);
|
var launchProfile = ResolveProfile(launchProfileName);
|
||||||
var runOptions = launchProfile.Options;
|
var runOptions = launchProfile.Options;
|
||||||
currentSession = await transcriptStore.CreateSessionAsync(runOptions, cancellationToken);
|
|
||||||
var recordedAudio = await recordedAudioStore.CreateSessionAsync(runOptions, cancellationToken);
|
|
||||||
var startedAt = inactivityClock.Now;
|
var startedAt = inactivityClock.Now;
|
||||||
|
currentSession = await transcriptStore.CreateSessionAsync(runOptions, startedAt, cancellationToken);
|
||||||
|
var recordedAudio = await recordedAudioStore.CreateSessionAsync(runOptions, cancellationToken);
|
||||||
var assistantContextPath = GetAssistantContextPath(startedAt, runOptions);
|
var assistantContextPath = GetAssistantContextPath(startedAt, runOptions);
|
||||||
var summaryPath = GetSummaryPath(startedAt, runOptions);
|
var summaryPath = GetSummaryPath(startedAt, runOptions);
|
||||||
currentMeetingNote = await meetingNoteStore.SaveAsync(
|
currentMeetingNote = await meetingNoteStore.SaveAsync(
|
||||||
@@ -1491,12 +1491,12 @@ public sealed class MeetingRecordingCoordinator
|
|||||||
|
|
||||||
private string GetAssistantContextPath(DateTimeOffset startedAt, MeetingAssistantOptions runOptions)
|
private string GetAssistantContextPath(DateTimeOffset startedAt, MeetingAssistantOptions runOptions)
|
||||||
{
|
{
|
||||||
return GetMeetingArtifactPath(runOptions.Vault, runOptions.Vault.AssistantContextFolder, startedAt, "assistant-context");
|
return GetMeetingArtifactPath(runOptions.Vault, runOptions.Vault.AssistantContextFolder, startedAt, MeetingArtifactFileNames.Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSummaryPath(DateTimeOffset startedAt, MeetingAssistantOptions runOptions)
|
private string GetSummaryPath(DateTimeOffset startedAt, MeetingAssistantOptions runOptions)
|
||||||
{
|
{
|
||||||
return GetMeetingArtifactPath(runOptions.Vault, runOptions.Vault.SummariesFolder, startedAt, "summary");
|
return GetMeetingArtifactPath(runOptions.Vault, runOptions.Vault.SummariesFolder, startedAt, MeetingArtifactFileNames.Summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetMeetingArtifactPath(
|
private static string GetMeetingArtifactPath(
|
||||||
@@ -1506,7 +1506,7 @@ public sealed class MeetingRecordingCoordinator
|
|||||||
string artifactName)
|
string artifactName)
|
||||||
{
|
{
|
||||||
var folder = VaultPath.Resolve(vault, configuredFolder);
|
var folder = VaultPath.Resolve(vault, configuredFolder);
|
||||||
return Path.Combine(folder, $"{startedAt:yyyyMMdd-HHmm}-{artifactName}.md");
|
return Path.Combine(folder, MeetingArtifactFileNames.Create(startedAt, artifactName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldAbortRunOnStop(RecordingRun run, DateTimeOffset stoppedAt)
|
private static bool ShouldAbortRunOnStop(RecordingRun run, DateTimeOffset stoppedAt)
|
||||||
|
|||||||
@@ -17,17 +17,18 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
|||||||
|
|
||||||
public async Task<TranscriptSession> CreateSessionAsync(CancellationToken cancellationToken)
|
public async Task<TranscriptSession> CreateSessionAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await CreateSessionAsync(options, cancellationToken);
|
return await CreateSessionAsync(options, DateTimeOffset.Now, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TranscriptSession> CreateSessionAsync(
|
public async Task<TranscriptSession> CreateSessionAsync(
|
||||||
MeetingAssistantOptions options,
|
MeetingAssistantOptions options,
|
||||||
|
DateTimeOffset startedAt,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var folder = VaultPath.Resolve(options.Vault, options.Vault.TranscriptsFolder);
|
var folder = VaultPath.Resolve(options.Vault, options.Vault.TranscriptsFolder);
|
||||||
Directory.CreateDirectory(folder);
|
Directory.CreateDirectory(folder);
|
||||||
|
|
||||||
var fileName = $"{DateTimeOffset.Now:yyyyMMdd-HHmmss-fffffff}-transcript.md";
|
var fileName = MeetingArtifactFileNames.Create(startedAt, MeetingArtifactFileNames.Transcript);
|
||||||
var path = Path.Combine(folder, fileName);
|
var path = Path.Combine(folder, fileName);
|
||||||
await File.WriteAllTextAsync(path, $"# Meeting Transcript{Environment.NewLine}{Environment.NewLine}", cancellationToken);
|
await File.WriteAllTextAsync(path, $"# Meeting Transcript{Environment.NewLine}{Environment.NewLine}", cancellationToken);
|
||||||
logger.LogInformation("Created meeting transcript file {TranscriptPath}", path);
|
logger.LogInformation("Created meeting transcript file {TranscriptPath}", path);
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ Meeting Assistant SHALL allow a new recording to start after the previous run's
|
|||||||
|
|
||||||
Each run SHALL keep its own transcript session, meeting note path, artifact paths, options, audio buffer, live transcript buffer, and speaker mappings isolated from later runs.
|
Each run SHALL keep its own transcript session, meeting note path, artifact paths, options, audio buffer, live transcript buffer, and speaker mappings isolated from later runs.
|
||||||
|
|
||||||
Rapidly-created meeting note, transcript, assistant context, and summary artifact filenames SHALL be distinct.
|
Generated meeting note, assistant context, transcript, and summary artifact filenames SHALL use the meeting start time with minute precision in the form `yyyyMMdd-HHmm-{type}.md`, where `type` is one of `note`, `context`, `transcript`, or `summary`.
|
||||||
|
|
||||||
#### Scenario: Hotkey stops recording with buffered audio
|
#### Scenario: Hotkey stops recording with buffered audio
|
||||||
- **WHEN** the user presses the hotkey to stop recording while captured audio is still buffered for transcription
|
- **WHEN** the user presses the hotkey to stop recording while captured audio is still buffered for transcription
|
||||||
@@ -123,6 +123,13 @@ Rapidly-created meeting note, transcript, assistant context, and summary artifac
|
|||||||
- **AND** final transcription and summary output from the stopped run use the stopped run's files
|
- **AND** final transcription and summary output from the stopped run use the stopped run's files
|
||||||
- **AND** live transcription buffers from the stopped run are not written to the new run
|
- **AND** live transcription buffers from the stopped run are not written to the new run
|
||||||
|
|
||||||
|
#### Scenario: Generated artifact names use minute precision
|
||||||
|
- **WHEN** a recording starts at `2026-05-30T09:30:30`
|
||||||
|
- **THEN** Meeting Assistant uses `20260530-0930-note.md` for the meeting note filename
|
||||||
|
- **AND** uses `20260530-0930-transcript.md` for the transcript filename
|
||||||
|
- **AND** uses `20260530-0930-context.md` for the assistant context filename
|
||||||
|
- **AND** uses `20260530-0930-summary.md` for the summary filename
|
||||||
|
|
||||||
### Requirement: Transcripts are written to the configured vault folder
|
### Requirement: Transcripts are written to the configured vault folder
|
||||||
Meeting Assistant SHALL write live transcript text to a markdown file in the configured vault folder.
|
Meeting Assistant SHALL write live transcript text to a markdown file in the configured vault folder.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user