feat(macos): enable meeting integrations

This commit is contained in:
dh
2026-08-06 08:19:26 +02:00
parent 9542c05ea4
commit 2af7933a1b
27 changed files with 1999 additions and 227 deletions
@@ -1,7 +1,4 @@
using System.Collections.Concurrent;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Speakers;
@@ -46,6 +43,25 @@ public sealed record ScreenshotOcrResult
public sealed record ScreenshotCropCoordinates(int X, int Y, int Width, int Height);
public interface IScreenshotImageCropper
{
Task<string?> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken);
}
internal sealed class UnavailableScreenshotImageCropper : IScreenshotImageCropper
{
public Task<string?> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken)
{
return Task.FromResult<string?>(null);
}
}
public interface IMeetingScreenshotService
{
Task<MeetingScreenshotCaptureResult> CaptureAsync(
@@ -147,6 +163,7 @@ public sealed partial class MeetingScreenshotService :
IMeetingNoteImageOcrService
{
private readonly IActiveWindowScreenshotCapture screenshotCapture;
private readonly IScreenshotImageCropper screenshotImageCropper;
private readonly IMeetingArtifactStore artifactStore;
private readonly IMeetingNoteStore meetingNoteStore;
private readonly ISpeakerIdentityAttendeeCanonicalizer attendeeCanonicalizer;
@@ -162,6 +179,7 @@ public sealed partial class MeetingScreenshotService :
IMeetingNoteStore meetingNoteStore,
ISpeakerIdentityAttendeeCanonicalizer attendeeCanonicalizer,
IScreenshotOcrClient ocrClient,
IScreenshotImageCropper screenshotImageCropper,
ILogger<MeetingScreenshotService> logger,
IMeetingWorkflowEngine? meetingWorkflowEngine = null)
{
@@ -171,6 +189,7 @@ public sealed partial class MeetingScreenshotService :
this.attendeeCanonicalizer = attendeeCanonicalizer;
this.meetingWorkflowEngine = meetingWorkflowEngine ?? NoopMeetingWorkflowEngine.Instance;
this.ocrClient = ocrClient;
this.screenshotImageCropper = screenshotImageCropper;
this.logger = logger;
}
@@ -639,17 +658,20 @@ public sealed partial class MeetingScreenshotService :
return "";
}
if (!OperatingSystem.IsWindows())
{
logger.LogWarning(
"Ignoring screenshot crop coordinates for {ScreenshotPath} because image cropping is only supported on Windows",
screenshotPath);
return "";
}
try
{
var croppedPath = await SaveCroppedScreenshotAsync(screenshotPath, crop, cancellationToken);
var croppedPath = await screenshotImageCropper.SaveCroppedScreenshotAsync(
screenshotPath,
crop,
cancellationToken);
if (croppedPath is null)
{
logger.LogWarning(
"Ignoring screenshot crop coordinates for {ScreenshotPath} because no image cropper is available on this host",
screenshotPath);
return "";
}
var relativePath = ToMarkdownPath(Path.GetRelativePath(
Path.GetDirectoryName(assistantContextPath)!,
croppedPath));
@@ -657,7 +679,7 @@ public sealed partial class MeetingScreenshotService :
Environment.NewLine +
Environment.NewLine;
}
catch (Exception exception) when (exception is InvalidDataException or ArgumentException or ExternalException)
catch (Exception exception) when (exception is InvalidDataException or ArgumentException)
{
logger.LogWarning(
exception,
@@ -667,39 +689,6 @@ public sealed partial class MeetingScreenshotService :
}
}
#pragma warning disable CA1416
private static Task<string> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using var original = new Bitmap(screenshotPath);
ValidateCrop(crop, original.Width, original.Height);
var cropRectangle = new Rectangle(crop.X, crop.Y, crop.Width, crop.Height);
using var cropped = original.Clone(cropRectangle, original.PixelFormat);
var croppedPath = Path.Combine(
Path.GetDirectoryName(screenshotPath)!,
$"{Path.GetFileNameWithoutExtension(screenshotPath)}-cropped.png");
cropped.Save(croppedPath, ImageFormat.Png);
return Task.FromResult(croppedPath);
}
#pragma warning restore CA1416
private static void ValidateCrop(ScreenshotCropCoordinates crop, int imageWidth, int imageHeight)
{
if (crop.X < 0 ||
crop.Y < 0 ||
crop.Width <= 0 ||
crop.Height <= 0 ||
crop.X + crop.Width > imageWidth ||
crop.Y + crop.Height > imageHeight)
{
throw new InvalidDataException(
$"Screenshot crop {crop.X},{crop.Y},{crop.Width},{crop.Height} is outside image bounds {imageWidth}x{imageHeight}.");
}
}
private void TrackOcr(
MeetingSessionArtifacts artifacts,
CancellationTokenSource cancellation,
@@ -0,0 +1,55 @@
#if WINDOWS
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace MeetingAssistant.Screenshots;
internal sealed class WindowsScreenshotImageCropper : IScreenshotImageCropper
{
#pragma warning disable CA1416
public Task<string?> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using var original = new Bitmap(screenshotPath);
ValidateCrop(crop, original.Width, original.Height);
var cropRectangle = new Rectangle(crop.X, crop.Y, crop.Width, crop.Height);
using var cropped = original.Clone(cropRectangle, original.PixelFormat);
var croppedPath = Path.Combine(
Path.GetDirectoryName(screenshotPath)!,
$"{Path.GetFileNameWithoutExtension(screenshotPath)}-cropped.png");
cropped.Save(croppedPath, ImageFormat.Png);
return Task.FromResult<string?>(croppedPath);
}
catch (ExternalException exception)
{
throw new InvalidDataException(
$"The screenshot crop {crop.X},{crop.Y},{crop.Width},{crop.Height} could not be applied.",
exception);
}
}
#pragma warning restore CA1416
private static void ValidateCrop(
ScreenshotCropCoordinates crop,
int imageWidth,
int imageHeight)
{
if (crop.X < 0 ||
crop.Y < 0 ||
crop.Width <= 0 ||
crop.Height <= 0 ||
crop.X + crop.Width > imageWidth ||
crop.Y + crop.Height > imageHeight)
{
throw new InvalidDataException(
$"Screenshot crop {crop.X},{crop.Y},{crop.Width},{crop.Height} is outside image bounds {imageWidth}x{imageHeight}.");
}
}
}
#endif