Improve toast duration and speaker override samples
PR and Push Build/Test / build-and-test (push) Failing after 9m48s

This commit is contained in:
2026-06-05 10:52:08 +02:00
parent 351ed2eb50
commit 1a89382f02
13 changed files with 162 additions and 33 deletions
@@ -1,6 +1,7 @@
#if WINDOWS
using System.Collections.Concurrent;
using CommunityToolkit.WinUI.Notifications;
using MeetingAssistant.Notifications;
using MeetingAssistant.Recording;
namespace MeetingAssistant.Calendar;
@@ -44,7 +45,7 @@ public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptServic
{
toast.Group = NotificationGroup;
toast.Tag = promptId;
toast.ExpirationTime = request.Meeting.End;
toast.ExpirationTime = MeetingToastExpirationPolicy.StartRecordingPromptExpiration(DateTimeOffset.Now);
});
logger.LogInformation(
"Displayed native Windows meeting-start notification {PromptId} for {Subject}",
@@ -134,6 +135,8 @@ public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptServic
return new ToastContentBuilder()
.AddArgument("source", NotificationSource)
.AddArgument("promptId", promptId)
.SetToastScenario(ToastScenario.Reminder)
.SetToastDuration(ToastDuration.Long)
.AddText("Record meeting?")
.AddText($"{request.Meeting.Subject} starts at {request.Meeting.Start.LocalDateTime:t}.")
.AddButton(yesButton)
+1 -1
View File
@@ -36,7 +36,7 @@ public sealed class AutomationOptions
public sealed class CalendarRecordingPromptOptions
{
public bool Enabled { get; set; }
public bool Enabled { get; set; } = true;
public TimeSpan SyncInterval { get; set; } = TimeSpan.FromMinutes(30);
@@ -0,0 +1,14 @@
namespace MeetingAssistant.Notifications;
internal static class MeetingToastExpirationPolicy
{
public static DateTimeOffset StopReminderExpiration(DateTimeOffset now)
{
return now.AddMinutes(1);
}
public static DateTimeOffset StartRecordingPromptExpiration(DateTimeOffset now)
{
return now.AddMinutes(5);
}
}
@@ -1,6 +1,7 @@
#if WINDOWS
using System.Collections.Concurrent;
using CommunityToolkit.WinUI.Notifications;
using MeetingAssistant.Notifications;
namespace MeetingAssistant.Recording;
@@ -48,7 +49,7 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
{
toast.Group = NotificationGroup;
toast.Tag = promptId;
toast.ExpirationTime = DateTimeOffset.Now.AddMinutes(15);
toast.ExpirationTime = MeetingToastExpirationPolicy.StopReminderExpiration(DateTimeOffset.Now);
});
logger.LogInformation(
"Displayed native Windows inactivity notification {PromptId} at threshold {Threshold}",
@@ -138,6 +139,8 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
return new ToastContentBuilder()
.AddArgument("source", NotificationSource)
.AddArgument("promptId", promptId)
.SetToastScenario(ToastScenario.Reminder)
.SetToastDuration(ToastDuration.Long)
.AddText("Stop meeting?")
.AddText($"No transcript text has arrived for {FormatDuration(request.InactivityDuration)}.")
.AddButton(yesButton)
@@ -79,6 +79,17 @@ public sealed class SpeakerIdentityService : ISpeakerIdentificationService
var meetingReference = CreateReference(request.MeetingNote, now);
var snippetResolution = await ResolveOverrideSnippetAsync(request, sourceLabel, cancellationToken);
var snippet = snippetResolution.WavBytes;
if (snippet.Length > 0 && !await matchValidator.ValidateSampleAsync(snippet, cancellationToken))
{
logger.LogInformation(
"Speaker override from {SourceSpeaker} to {TargetSpeaker} rejected source sample after secondary validation: sample source {SampleSource}, sample bytes {SampleBytes}",
sourceLabel,
targetName,
snippetResolution.Source,
snippet.Length);
snippet = [];
}
var target = await FindIdentityByAcceptedNameAsync(context, targetName, cancellationToken);
var sourceCandidate = await FindCurrentRunCandidateAsync(
context,
@@ -277,20 +288,10 @@ public sealed class SpeakerIdentityService : ISpeakerIdentificationService
if (snippet.Length == 0)
{
snippetSource = "extracted";
var span = SpeakerSampleSpanSelector.SelectBestContinuousSpan(
request.Segments,
(snippet, _) = await ExtractBestContinuousSampleAsync(
request,
speaker,
options.MaximumSampleSegmentGap,
options.MinimumSampleSpeechDuration);
logger.LogInformation(
"Speaker identity processing extracting sample for {Speaker}: selected {SegmentCount} segment(s), span {SpanDuration}, minimum {MinimumDuration}",
speaker,
span.Count,
SpeakerSampleSpanSelector.SpanDuration(span),
options.MinimumSampleSpeechDuration);
snippet = await snippetExtractor.ExtractSnippetAsync(
request.AudioPath,
span,
"Speaker identity processing",
cancellationToken);
}
@@ -521,13 +522,39 @@ public sealed class SpeakerIdentityService : ISpeakerIdentificationService
return new SpeakerSnippetResolution([], "missing-segments", SegmentCount: 0, Score: null, Duration: TimeSpan.Zero);
}
var snippet = await snippetExtractor.ExtractSnippetAsync(request.AudioPath, segments, cancellationToken);
var (snippet, span) = await ExtractBestContinuousSampleAsync(
request,
sourceSpeaker,
"Speaker override",
cancellationToken);
return new SpeakerSnippetResolution(
snippet,
"extracted-from-recording",
segments.Count,
span.Count,
Score: null,
segments.Max(segment => segment.End) - segments.Min(segment => segment.Start));
SpeakerSampleSpanSelector.SpanDuration(span));
}
private async Task<(byte[] Snippet, IReadOnlyList<TranscriptionSegment> Span)> ExtractBestContinuousSampleAsync(
SpeakerIdentificationRequest request,
string speaker,
string operation,
CancellationToken cancellationToken)
{
var span = SpeakerSampleSpanSelector.SelectBestContinuousSpan(
request.Segments,
speaker,
options.MaximumSampleSegmentGap,
options.MinimumSampleSpeechDuration);
logger.LogInformation(
"{Operation} extracting fallback sample for {Speaker}: selected {SegmentCount} segment(s), span {SpanDuration}, minimum {MinimumDuration}",
operation,
speaker,
span.Count,
SpeakerSampleSpanSelector.SpanDuration(span),
options.MinimumSampleSpeechDuration);
var snippet = await snippetExtractor.ExtractSnippetAsync(request.AudioPath, span, cancellationToken);
return (snippet, span);
}
private static async Task<SpeakerIdentity?> FindIdentityByAcceptedNameAsync(
+1 -1
View File
@@ -147,7 +147,7 @@
"RulesPath": "C:\\Manuel\\meeting-assistant\\meeting-rules.local.yaml"
},
"CalendarRecordingPrompts": {
"Enabled": false,
"Enabled": true,
"SyncInterval": "00:30:00",
"PromptWindow": "00:05:00"
},